Merge: Advanced Analytics Dashboard (Agent 7) - resolved conflicts
This commit is contained in:
commit
207e61b06c
36 changed files with 4802 additions and 328 deletions
494
bun.lock
494
bun.lock
|
|
@ -5,31 +5,29 @@
|
|||
"": {
|
||||
"name": "localgreenchain",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.937.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.937.0",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.1",
|
||||
"@tanstack/react-query": "^4.0.10",
|
||||
"classnames": "^2.3.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drupal-jsonapi-params": "^1.2.2",
|
||||
"html-react-parser": "^1.2.7",
|
||||
"multer": "^2.0.2",
|
||||
"next": "^12.2.3",
|
||||
"next-drupal": "^1.6.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.8.6",
|
||||
"sharp": "^0.34.5",
|
||||
"recharts": "^3.4.1",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint-config-next": "^12.0.10",
|
||||
"jest": "^29.5.0",
|
||||
|
|
@ -43,90 +41,6 @@
|
|||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
||||
|
||||
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
|
||||
|
||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.937.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/credential-provider-node": "3.936.0", "@aws-sdk/middleware-bucket-endpoint": "3.936.0", "@aws-sdk/middleware-expect-continue": "3.936.0", "@aws-sdk/middleware-flexible-checksums": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-location-constraint": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-sdk-s3": "3.936.0", "@aws-sdk/middleware-ssec": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/signature-v4-multi-region": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ioeNe6HSc7PxjsUQY7foSHmgesxM5KwAeUtPhIHgKx99nrM+7xYCfW4FMvHypUzz7ZOvqlCdH7CEAZ8ParBvVg=="],
|
||||
|
||||
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.936.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-0G73S2cDqYwJVvqL08eakj79MZG2QRaB56Ul8/Ps9oQxllr7DMI1IQ/N3j3xjxgpq/U36pkoFZ8aK1n7Sbr3IQ=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-dKajFuaugEA5i9gCKzOaVy9uTeZcApE+7Z5wdcZ6j40523fY1a56khDAUYkCfwqa7sHci4ccmxBkAo+fW1RChA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-5FguODLXG1tWx/x8fBxH+GVrk7Hey2LbXV5h9SFzYCx/2h50URBm0+9hndg0Rd23+xzYe14F6SI9HA9c1sPnjg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/credential-provider-env": "3.936.0", "@aws-sdk/credential-provider-http": "3.936.0", "@aws-sdk/credential-provider-login": "3.936.0", "@aws-sdk/credential-provider-process": "3.936.0", "@aws-sdk/credential-provider-sso": "3.936.0", "@aws-sdk/credential-provider-web-identity": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-TbUv56ERQQujoHcLMcfL0Q6bVZfYF83gu/TjHkVkdSlHPOIKaG/mhE2XZSQzXv1cud6LlgeBbfzVAxJ+HPpffg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8DVrdRqPyUU66gfV7VZNToh56ZuO5D6agWrkLQE/xbLJOm2RbeRgh6buz7CqV8ipRd6m+zCl9mM4F3osQLZn8Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.936.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.936.0", "@aws-sdk/credential-provider-http": "3.936.0", "@aws-sdk/credential-provider-ini": "3.936.0", "@aws-sdk/credential-provider-process": "3.936.0", "@aws-sdk/credential-provider-sso": "3.936.0", "@aws-sdk/credential-provider-web-identity": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-rk/2PCtxX9xDsQW8p5Yjoca3StqmQcSfkmD7nQ61AqAHL1YgpSQWqHE+HjfGGiHDYKG7PvE33Ku2GyA7lEIJAw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GpA4AcHb96KQK2PSPUyvChvrsEKiLhQ5NWjeef2IZ3Jc8JoosiedYqp6yhZR+S8cTysuvx56WyJIJc8y8OTrLA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.936.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.936.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/token-providers": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wHlEAJJvtnSyxTfNhN98JcU4taA1ED2JvuI2eePgawqBwS/Tzi0mhED1lvNIaWOkjfLd+nHALwszGrtJwEq4yQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-v3qHAuoODkoRXsAF4RG+ZVO6q2P9yYBT4GMpMEfU9wXVNn7AIfwZgTwzSUfnjNiGva5BKleWVpRpJ9DeuLFbUg=="],
|
||||
|
||||
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg=="],
|
||||
|
||||
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA=="],
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.936.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-l3GG6CrSQtMCM6fWY7foV3JQv0WJWT+3G6PSP3Ceb/KEE/5Lz5PrYFXTBf+bVoYL1b0bGjGajcgAXpstBmtHtQ=="],
|
||||
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="],
|
||||
|
||||
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw=="],
|
||||
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="],
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA=="],
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-UQs/pVq4cOygsnKON0pOdSKIWkfgY0dzq4h+fR+xHi/Ng3XzxPJhWeAE6tDsKrcyQc1X8UdSbS70XkfGYr5hng=="],
|
||||
|
||||
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA=="],
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.936.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="],
|
||||
|
||||
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.937.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-format-url": "3.936.0", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-AvsCt6FnnKTpkmzDA1pFzmXPyxbGBdtllOIY0mL1iNSVZ3d7SoJKZH4NaqlcgUtbYG9zVh6QfLWememj1yEAmw=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.936.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8qS0GFUqkmwO7JZ0P8tdluBmt1UTfYUah8qJXGzNh9n1Pcb0AIeT117cCSiCUtwk+gDbJvd4hhRIhJCNr5wgjg=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-vvw8+VXk0I+IsoxZw0mX9TMJawUJvEsg3EF7zcCSetwhNPAU8Xmlhv7E/sN/FgSmm7b7DsqKoW6rVtQiCs1PWQ=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="],
|
||||
|
||||
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA=="],
|
||||
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="],
|
||||
|
||||
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.936.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
|
|
@ -197,8 +111,6 @@
|
|||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
|
@ -213,56 +125,6 @@
|
|||
|
||||
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
|
||||
|
||||
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
|
||||
|
|
@ -341,6 +203,8 @@
|
|||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.10.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
|
||||
|
|
@ -351,107 +215,9 @@
|
|||
|
||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||
|
||||
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="],
|
||||
|
||||
"@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="],
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.18.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.6", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
|
||||
|
||||
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ=="],
|
||||
|
||||
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.5", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg=="],
|
||||
|
||||
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.6", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw=="],
|
||||
|
||||
"@smithy/hash-node": ["@smithy/hash-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA=="],
|
||||
|
||||
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q=="],
|
||||
|
||||
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
|
||||
|
||||
"@smithy/md5-js": ["@smithy/md5-js@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg=="],
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.12", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-serde": "^4.2.6", "@smithy/node-config-provider": "^4.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/service-error-classification": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ=="],
|
||||
|
||||
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ=="],
|
||||
|
||||
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw=="],
|
||||
|
||||
"@smithy/property-provider": ["@smithy/property-provider@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg=="],
|
||||
|
||||
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ=="],
|
||||
|
||||
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg=="],
|
||||
|
||||
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ=="],
|
||||
|
||||
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0" } }, "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ=="],
|
||||
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.5", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.8", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-stack": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.9.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA=="],
|
||||
|
||||
"@smithy/url-parser": ["@smithy/url-parser@4.2.5", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ=="],
|
||||
|
||||
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
|
||||
|
||||
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="],
|
||||
|
||||
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.14", "", { "dependencies": { "@smithy/config-resolver": "^4.4.3", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A=="],
|
||||
|
||||
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
|
||||
|
||||
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA=="],
|
||||
|
||||
"@smithy/util-retry": ["@smithy/util-retry@4.2.5", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg=="],
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.5.6", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ=="],
|
||||
|
||||
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
|
||||
|
||||
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g=="],
|
||||
|
||||
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.4.11", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw=="],
|
||||
|
||||
|
|
@ -471,18 +237,72 @@
|
|||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="],
|
||||
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
|
||||
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||
|
||||
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||
|
||||
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
|
||||
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||
|
||||
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||
|
||||
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||
|
||||
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||
|
||||
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||
|
||||
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||
|
||||
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||
|
||||
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
|
||||
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||
|
||||
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||
|
||||
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
|
||||
|
|
@ -493,30 +313,18 @@
|
|||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/multer": ["@types/multer@2.0.0", "", { "dependencies": { "@types/express": "*" } }, "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw=="],
|
||||
|
||||
"@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
|
||||
|
||||
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/react": ["@types/react@17.0.90", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", "csstype": "^3.2.2" } }, "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw=="],
|
||||
|
||||
"@types/scheduler": ["@types/scheduler@0.16.8", "", {}, "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="],
|
||||
|
||||
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@types/sharp": ["@types/sharp@0.32.0", "", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="],
|
||||
|
||||
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
|
||||
|
||||
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
||||
|
|
@ -551,8 +359,6 @@
|
|||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
|
@ -605,8 +411,6 @@
|
|||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
|
@ -619,8 +423,6 @@
|
|||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
|
@ -651,6 +453,8 @@
|
|||
|
||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="],
|
||||
|
||||
"collect-v8-coverage": ["collect-v8-coverage@1.0.3", "", {}, "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw=="],
|
||||
|
|
@ -663,8 +467,6 @@
|
|||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="],
|
||||
|
|
@ -675,6 +477,68 @@
|
|||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||
|
||||
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||
|
||||
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||
|
||||
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
|
||||
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||
|
||||
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||
|
||||
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||
|
||||
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
|
||||
|
||||
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
|
||||
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
|
||||
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||
|
||||
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
|
||||
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||
|
||||
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||
|
||||
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
|
@ -683,8 +547,12 @@
|
|||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
|
@ -695,7 +563,7 @@
|
|||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||
|
||||
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
|
||||
|
||||
|
|
@ -747,6 +615,8 @@
|
|||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.42.0", "", {}, "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
|
@ -785,6 +655,8 @@
|
|||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="],
|
||||
|
|
@ -799,8 +671,6 @@
|
|||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="],
|
||||
|
|
@ -889,8 +759,12 @@
|
|||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
|
||||
|
|
@ -905,6 +779,8 @@
|
|||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
|
@ -1093,18 +969,12 @@
|
|||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||
|
|
@ -1113,12 +983,8 @@
|
|||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"multer": ["multer@2.0.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
|
@ -1243,18 +1109,26 @@
|
|||
|
||||
"react-property": ["react-property@2.0.0", "", {}, "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"recharts": ["recharts@3.4.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g=="],
|
||||
|
||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||
|
||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
|
||||
|
|
@ -1267,16 +1141,20 @@
|
|||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.20.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
|
@ -1287,8 +1165,6 @@
|
|||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
|
@ -1325,8 +1201,6 @@
|
|||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||
|
||||
"string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
|
@ -1343,8 +1217,6 @@
|
|||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
|
@ -1353,8 +1225,6 @@
|
|||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.1", "", { "dependencies": { "style-to-object": "0.3.0" } }, "sha512-RJ18Z9t2B02sYhZtfWKQq5uplVctgvjTfLWT7+Eb1zjUjIrWzX5SdlkwLGQozrqarTmEzJJ/YmdNJCUNI47elg=="],
|
||||
|
||||
"style-to-object": ["style-to-object@0.3.0", "", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA=="],
|
||||
|
|
@ -1377,6 +1247,8 @@
|
|||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
|
||||
|
|
@ -1399,8 +1271,6 @@
|
|||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
|
@ -1409,8 +1279,6 @@
|
|||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||
|
||||
"typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
|
@ -1427,6 +1295,8 @@
|
|||
|
||||
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
|
@ -1449,8 +1319,6 @@
|
|||
|
||||
"write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
|
@ -1461,12 +1329,6 @@
|
|||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
|
@ -1477,8 +1339,6 @@
|
|||
|
||||
"@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||
|
||||
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
|
@ -1489,6 +1349,8 @@
|
|||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
|
||||
|
||||
"eslint/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||
|
|
@ -1533,8 +1395,6 @@
|
|||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
|
@ -1551,24 +1411,12 @@
|
|||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ export default function EnvironmentalForm({
|
|||
section: K,
|
||||
updates: Partial<GrowingEnvironment[K]>
|
||||
) => {
|
||||
const currentSection = value[section] || {};
|
||||
onChange({
|
||||
...value,
|
||||
[section]: {
|
||||
...value[section],
|
||||
...currentSection,
|
||||
...updates,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
229
components/analytics/DataTable.tsx
Normal file
229
components/analytics/DataTable.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* Data Table Component
|
||||
* Sortable and filterable data table for analytics
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
interface Column {
|
||||
key: string;
|
||||
header: string;
|
||||
sortable?: boolean;
|
||||
render?: (value: any, row: any) => React.ReactNode;
|
||||
width?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
data: any[];
|
||||
columns: Column[];
|
||||
title?: string;
|
||||
pageSize?: number;
|
||||
showSearch?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
}
|
||||
|
||||
type SortDirection = 'asc' | 'desc' | null;
|
||||
|
||||
export default function DataTable({
|
||||
data,
|
||||
columns,
|
||||
title,
|
||||
pageSize = 10,
|
||||
showSearch = true,
|
||||
searchPlaceholder = 'Search...',
|
||||
}: DataTableProps) {
|
||||
const [sortKey, setSortKey] = useState<string | null>(null);
|
||||
const [sortDir, setSortDir] = useState<SortDirection>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!search) return data;
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
return data.filter((row) =>
|
||||
columns.some((col) => {
|
||||
const value = row[col.key];
|
||||
return String(value).toLowerCase().includes(searchLower);
|
||||
})
|
||||
);
|
||||
}, [data, columns, search]);
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
if (!sortKey || !sortDir) return filteredData;
|
||||
|
||||
return [...filteredData].sort((a, b) => {
|
||||
const aVal = a[sortKey];
|
||||
const bVal = b[sortKey];
|
||||
|
||||
if (aVal === bVal) return 0;
|
||||
if (aVal === null || aVal === undefined) return 1;
|
||||
if (bVal === null || bVal === undefined) return -1;
|
||||
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return sortDir === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [filteredData, sortKey, sortDir]);
|
||||
|
||||
const paginatedData = useMemo(() => {
|
||||
const start = page * pageSize;
|
||||
return sortedData.slice(start, start + pageSize);
|
||||
}, [sortedData, page, pageSize]);
|
||||
|
||||
const totalPages = Math.ceil(sortedData.length / pageSize);
|
||||
|
||||
const handleSort = (key: string) => {
|
||||
if (sortKey === key) {
|
||||
if (sortDir === 'asc') setSortDir('desc');
|
||||
else if (sortDir === 'desc') {
|
||||
setSortKey(null);
|
||||
setSortDir(null);
|
||||
}
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIcon = (key: string) => {
|
||||
if (sortKey !== key) {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (sortDir === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const alignClasses = {
|
||||
left: 'text-left',
|
||||
center: 'text-center',
|
||||
right: 'text-right',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900">{title}</h3>}
|
||||
{showSearch && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={`px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider ${
|
||||
alignClasses[col.align || 'left']
|
||||
} ${col.sortable !== false ? 'cursor-pointer hover:bg-gray-100' : ''}`}
|
||||
style={{ width: col.width }}
|
||||
onClick={() => col.sortable !== false && handleSort(col.key)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{col.header}</span>
|
||||
{col.sortable !== false && getSortIcon(col.key)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{paginatedData.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-6 py-8 text-center text-gray-500">
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
paginatedData.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="hover:bg-gray-50">
|
||||
{columns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={`px-6 py-4 whitespace-nowrap text-sm text-gray-900 ${
|
||||
alignClasses[col.align || 'left']
|
||||
}`}
|
||||
>
|
||||
{col.render ? col.render(row[col.key], row) : row[col.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">
|
||||
Showing {page * pageSize + 1} to {Math.min((page + 1) * pageSize, sortedData.length)} of{' '}
|
||||
{sortedData.length} results
|
||||
</span>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 border border-gray-200 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 border border-gray-200 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
components/analytics/DateRangePicker.tsx
Normal file
47
components/analytics/DateRangePicker.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Date Range Picker Component
|
||||
* Allows selection of time range for analytics
|
||||
*/
|
||||
|
||||
import { TimeRange } from '../../lib/analytics/types';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
value: TimeRange;
|
||||
onChange: (range: TimeRange) => void;
|
||||
showCustom?: boolean;
|
||||
}
|
||||
|
||||
const timeRangeOptions: { value: TimeRange; label: string }[] = [
|
||||
{ value: '7d', label: 'Last 7 days' },
|
||||
{ value: '30d', label: 'Last 30 days' },
|
||||
{ value: '90d', label: 'Last 90 days' },
|
||||
{ value: '365d', label: 'Last year' },
|
||||
{ value: 'all', label: 'All time' },
|
||||
];
|
||||
|
||||
export default function DateRangePicker({
|
||||
value,
|
||||
onChange,
|
||||
showCustom = false,
|
||||
}: DateRangePickerProps) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Time range:</span>
|
||||
<div className="inline-flex rounded-lg border border-gray-200 bg-white">
|
||||
{timeRangeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors first:rounded-l-lg last:rounded-r-lg ${
|
||||
value === option.value
|
||||
? 'bg-green-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
165
components/analytics/FilterPanel.tsx
Normal file
165
components/analytics/FilterPanel.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* Filter Panel Component
|
||||
* Provides filtering options for analytics data
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface FilterConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'multiselect' | 'search';
|
||||
options?: FilterOption[];
|
||||
}
|
||||
|
||||
interface FilterPanelProps {
|
||||
filters: FilterConfig[];
|
||||
values: Record<string, any>;
|
||||
onChange: (values: Record<string, any>) => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export default function FilterPanel({
|
||||
filters,
|
||||
values,
|
||||
onChange,
|
||||
onReset,
|
||||
}: FilterPanelProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
onChange({ ...values, [key]: value });
|
||||
};
|
||||
|
||||
const handleMultiSelect = (key: string, value: string) => {
|
||||
const current = values[key] || [];
|
||||
const updated = current.includes(value)
|
||||
? current.filter((v: string) => v !== value)
|
||||
: [...current, value];
|
||||
handleChange(key, updated);
|
||||
};
|
||||
|
||||
const activeFilterCount = Object.values(values).filter(
|
||||
(v) => v && (Array.isArray(v) ? v.length > 0 : true)
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow border border-gray-200">
|
||||
{/* Header */}
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">Filters</span>
|
||||
{activeFilterCount > 0 && (
|
||||
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">
|
||||
{activeFilterCount} active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-400 transform transition-transform ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Filter content */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 py-4 border-t border-gray-200 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.key}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{filter.label}
|
||||
</label>
|
||||
{filter.type === 'select' && filter.options && (
|
||||
<select
|
||||
value={values[filter.key] || ''}
|
||||
onChange={(e) => handleChange(filter.key, e.target.value || null)}
|
||||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
>
|
||||
<option value="">All</option>
|
||||
{filter.options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{filter.type === 'multiselect' && filter.options && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{filter.options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => handleMultiSelect(filter.key, opt.value)}
|
||||
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
|
||||
(values[filter.key] || []).includes(opt.value)
|
||||
? 'bg-green-500 text-white border-green-500'
|
||||
: 'bg-white text-gray-600 border-gray-200 hover:border-green-300'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filter.type === 'search' && (
|
||||
<input
|
||||
type="text"
|
||||
value={values[filter.key] || ''}
|
||||
onChange={(e) => handleChange(filter.key, e.target.value || null)}
|
||||
placeholder={`Search ${filter.label.toLowerCase()}...`}
|
||||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-2 pt-2 border-t border-gray-100">
|
||||
{onReset && (
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
Reset filters
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="px-4 py-2 text-sm bg-green-500 text-white rounded-lg hover:bg-green-600"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
components/analytics/KPICard.tsx
Normal file
129
components/analytics/KPICard.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* KPI Card Component
|
||||
* Displays key performance indicators with trend indicators
|
||||
*/
|
||||
|
||||
import { TrendDirection } from '../../lib/analytics/types';
|
||||
|
||||
interface KPICardProps {
|
||||
title: string;
|
||||
value: number | string;
|
||||
unit?: string;
|
||||
change?: number;
|
||||
changePercent?: number;
|
||||
trend?: TrendDirection;
|
||||
color?: 'green' | 'blue' | 'purple' | 'orange' | 'red' | 'teal';
|
||||
icon?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const colorClasses = {
|
||||
green: {
|
||||
bg: 'bg-green-50',
|
||||
text: 'text-green-600',
|
||||
icon: 'text-green-500',
|
||||
},
|
||||
blue: {
|
||||
bg: 'bg-blue-50',
|
||||
text: 'text-blue-600',
|
||||
icon: 'text-blue-500',
|
||||
},
|
||||
purple: {
|
||||
bg: 'bg-purple-50',
|
||||
text: 'text-purple-600',
|
||||
icon: 'text-purple-500',
|
||||
},
|
||||
orange: {
|
||||
bg: 'bg-orange-50',
|
||||
text: 'text-orange-600',
|
||||
icon: 'text-orange-500',
|
||||
},
|
||||
red: {
|
||||
bg: 'bg-red-50',
|
||||
text: 'text-red-600',
|
||||
icon: 'text-red-500',
|
||||
},
|
||||
teal: {
|
||||
bg: 'bg-teal-50',
|
||||
text: 'text-teal-600',
|
||||
icon: 'text-teal-500',
|
||||
},
|
||||
};
|
||||
|
||||
export default function KPICard({
|
||||
title,
|
||||
value,
|
||||
unit,
|
||||
change,
|
||||
changePercent,
|
||||
trend = 'stable',
|
||||
color = 'green',
|
||||
icon,
|
||||
loading = false,
|
||||
}: KPICardProps) {
|
||||
const classes = colorClasses[color];
|
||||
|
||||
const getTrendIcon = () => {
|
||||
if (trend === 'up') {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (trend === 'down') {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const getTrendColor = () => {
|
||||
if (trend === 'up') return 'text-green-600';
|
||||
if (trend === 'down') return 'text-red-600';
|
||||
return 'text-gray-500';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`${classes.bg} rounded-lg p-6 animate-pulse`}>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
|
||||
<div className="h-8 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/3"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${classes.bg} rounded-lg p-6 transition-all hover:shadow-md`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-sm font-medium text-gray-600">{title}</p>
|
||||
{icon && <span className={classes.icon}>{icon}</span>}
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<p className={`text-3xl font-bold ${classes.text}`}>{value}</p>
|
||||
{unit && <span className="text-sm text-gray-500">{unit}</span>}
|
||||
</div>
|
||||
{(change !== undefined || changePercent !== undefined) && (
|
||||
<div className={`flex items-center mt-2 space-x-1 ${getTrendColor()}`}>
|
||||
{getTrendIcon()}
|
||||
<span className="text-sm font-medium">
|
||||
{changePercent !== undefined
|
||||
? `${changePercent > 0 ? '+' : ''}${changePercent.toFixed(1)}%`
|
||||
: change !== undefined
|
||||
? `${change > 0 ? '+' : ''}${change}`
|
||||
: ''}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">vs prev period</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
105
components/analytics/TrendIndicator.tsx
Normal file
105
components/analytics/TrendIndicator.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Trend Indicator Component
|
||||
* Shows trend direction with visual indicators
|
||||
*/
|
||||
|
||||
import { TrendDirection } from '../../lib/analytics/types';
|
||||
|
||||
interface TrendIndicatorProps {
|
||||
direction: TrendDirection;
|
||||
value?: number;
|
||||
showLabel?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'w-3 h-3',
|
||||
md: 'w-4 h-4',
|
||||
lg: 'w-5 h-5',
|
||||
};
|
||||
|
||||
const textSizeClasses = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
};
|
||||
|
||||
export default function TrendIndicator({
|
||||
direction,
|
||||
value,
|
||||
showLabel = false,
|
||||
size = 'md',
|
||||
}: TrendIndicatorProps) {
|
||||
const iconSize = sizeClasses[size];
|
||||
const textSize = textSizeClasses[size];
|
||||
|
||||
const getColor = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return 'text-green-500';
|
||||
case 'down':
|
||||
return 'text-red-500';
|
||||
default:
|
||||
return 'text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getBgColor = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return 'bg-green-100';
|
||||
case 'down':
|
||||
return 'bg-red-100';
|
||||
default:
|
||||
return 'bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return (
|
||||
<svg className={iconSize} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 11l5-5m0 0l5 5m-5-5v12" />
|
||||
</svg>
|
||||
);
|
||||
case 'down':
|
||||
return (
|
||||
<svg className={iconSize} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 13l-5 5m0 0l-5-5m5 5V6" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg className={iconSize} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return 'Increasing';
|
||||
case 'down':
|
||||
return 'Decreasing';
|
||||
default:
|
||||
return 'Stable';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center space-x-1.5 px-2 py-1 rounded-full ${getBgColor()}`}>
|
||||
<span className={getColor()}>{getIcon()}</span>
|
||||
{value !== undefined && (
|
||||
<span className={`font-medium ${getColor()} ${textSize}`}>
|
||||
{value > 0 ? '+' : ''}{value.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
{showLabel && (
|
||||
<span className={`${getColor()} ${textSize}`}>{getLabel()}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
components/analytics/charts/AreaChart.tsx
Normal file
98
components/analytics/charts/AreaChart.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Area Chart Component
|
||||
* Displays time series data as a filled area chart
|
||||
*/
|
||||
|
||||
import {
|
||||
AreaChart as RechartsAreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
interface AreaChartProps {
|
||||
data: any[];
|
||||
xKey: string;
|
||||
yKey: string | string[];
|
||||
title?: string;
|
||||
colors?: string[];
|
||||
height?: number;
|
||||
showGrid?: boolean;
|
||||
showLegend?: boolean;
|
||||
stacked?: boolean;
|
||||
gradient?: boolean;
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444'];
|
||||
|
||||
export default function AreaChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
title,
|
||||
colors = DEFAULT_COLORS,
|
||||
height = 300,
|
||||
showGrid = true,
|
||||
showLegend = true,
|
||||
stacked = false,
|
||||
gradient = true,
|
||||
formatter = (value) => value.toLocaleString(),
|
||||
}: AreaChartProps) {
|
||||
const yKeys = Array.isArray(yKey) ? yKey : [yKey];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RechartsAreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
{yKeys.map((key, index) => (
|
||||
<linearGradient key={key} id={`color${key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors[index % colors.length]} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={colors[index % colors.length]} stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />}
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
tick={{ fill: '#6b7280', fontSize: 12 }}
|
||||
tickLine={{ stroke: '#e5e7eb' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#6b7280', fontSize: 12 }}
|
||||
tickLine={{ stroke: '#e5e7eb' }}
|
||||
tickFormatter={formatter}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
formatter={(value: number) => [formatter(value), '']}
|
||||
/>
|
||||
{showLegend && <Legend />}
|
||||
{yKeys.map((key, index) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stackId={stacked ? 'stack' : undefined}
|
||||
stroke={colors[index % colors.length]}
|
||||
strokeWidth={2}
|
||||
fill={gradient ? `url(#color${key})` : colors[index % colors.length]}
|
||||
fillOpacity={gradient ? 1 : 0.6}
|
||||
/>
|
||||
))}
|
||||
</RechartsAreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
components/analytics/charts/BarChart.tsx
Normal file
99
components/analytics/charts/BarChart.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Bar Chart Component
|
||||
* Displays categorical data as bars
|
||||
*/
|
||||
|
||||
import {
|
||||
BarChart as RechartsBarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
|
||||
interface BarChartProps {
|
||||
data: any[];
|
||||
xKey: string;
|
||||
yKey: string | string[];
|
||||
title?: string;
|
||||
colors?: string[];
|
||||
height?: number;
|
||||
showGrid?: boolean;
|
||||
showLegend?: boolean;
|
||||
stacked?: boolean;
|
||||
horizontal?: boolean;
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444', '#06b6d4'];
|
||||
|
||||
export default function BarChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
title,
|
||||
colors = DEFAULT_COLORS,
|
||||
height = 300,
|
||||
showGrid = true,
|
||||
showLegend = true,
|
||||
stacked = false,
|
||||
horizontal = false,
|
||||
formatter = (value) => value.toLocaleString(),
|
||||
}: BarChartProps) {
|
||||
const yKeys = Array.isArray(yKey) ? yKey : [yKey];
|
||||
const layout = horizontal ? 'vertical' : 'horizontal';
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RechartsBarChart
|
||||
data={data}
|
||||
layout={layout}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />}
|
||||
{horizontal ? (
|
||||
<>
|
||||
<XAxis type="number" tick={{ fill: '#6b7280', fontSize: 12 }} tickFormatter={formatter} />
|
||||
<YAxis dataKey={xKey} type="category" tick={{ fill: '#6b7280', fontSize: 12 }} width={100} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XAxis dataKey={xKey} tick={{ fill: '#6b7280', fontSize: 12 }} />
|
||||
<YAxis tick={{ fill: '#6b7280', fontSize: 12 }} tickFormatter={formatter} />
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
formatter={(value: number) => [formatter(value), '']}
|
||||
/>
|
||||
{showLegend && yKeys.length > 1 && <Legend />}
|
||||
{yKeys.map((key, index) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
fill={colors[index % colors.length]}
|
||||
stackId={stacked ? 'stack' : undefined}
|
||||
radius={[4, 4, 0, 0]}
|
||||
>
|
||||
{yKeys.length === 1 &&
|
||||
data.map((entry, i) => (
|
||||
<Cell key={`cell-${i}`} fill={colors[i % colors.length]} />
|
||||
))}
|
||||
</Bar>
|
||||
))}
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
components/analytics/charts/Gauge.tsx
Normal file
91
components/analytics/charts/Gauge.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Gauge Chart Component
|
||||
* Displays a single value as a gauge/meter
|
||||
*/
|
||||
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
|
||||
|
||||
interface GaugeProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
title?: string;
|
||||
unit?: string;
|
||||
size?: number;
|
||||
colors?: { low: string; medium: string; high: string };
|
||||
thresholds?: { low: number; high: number };
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
low: '#ef4444',
|
||||
medium: '#f59e0b',
|
||||
high: '#10b981',
|
||||
};
|
||||
|
||||
export default function Gauge({
|
||||
value,
|
||||
max = 100,
|
||||
title,
|
||||
unit = '%',
|
||||
size = 200,
|
||||
colors = DEFAULT_COLORS,
|
||||
thresholds = { low: 33, high: 66 },
|
||||
}: GaugeProps) {
|
||||
const percentage = Math.min((value / max) * 100, 100);
|
||||
|
||||
// Determine color based on thresholds
|
||||
let color: string;
|
||||
if (percentage < thresholds.low) {
|
||||
color = colors.low;
|
||||
} else if (percentage < thresholds.high) {
|
||||
color = colors.medium;
|
||||
} else {
|
||||
color = colors.high;
|
||||
}
|
||||
|
||||
// Data for semi-circle gauge
|
||||
const gaugeData = [
|
||||
{ value: percentage, color },
|
||||
{ value: 100 - percentage, color: '#e5e7eb' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 flex flex-col items-center">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-2">{title}</h3>}
|
||||
<div className="relative" style={{ width: size, height: size / 2 + 20 }}>
|
||||
<ResponsiveContainer width="100%" height={size}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={gaugeData}
|
||||
cx="50%"
|
||||
cy="100%"
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={size * 0.3}
|
||||
outerRadius={size * 0.4}
|
||||
paddingAngle={0}
|
||||
dataKey="value"
|
||||
>
|
||||
{gaugeData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} stroke="none" />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center justify-end pb-2"
|
||||
style={{ top: size * 0.2 }}
|
||||
>
|
||||
<span className="text-3xl font-bold" style={{ color }}>
|
||||
{value.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between w-full mt-2 px-4 text-xs text-gray-500">
|
||||
<span>0</span>
|
||||
<span>{max / 2}</span>
|
||||
<span>{max}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
components/analytics/charts/Heatmap.tsx
Normal file
134
components/analytics/charts/Heatmap.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* Heatmap Component
|
||||
* Displays data intensity across a grid
|
||||
*/
|
||||
|
||||
interface HeatmapCell {
|
||||
x: string;
|
||||
y: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface HeatmapProps {
|
||||
data: HeatmapCell[];
|
||||
title?: string;
|
||||
xLabels: string[];
|
||||
yLabels: string[];
|
||||
colorRange?: { min: string; max: string };
|
||||
height?: number;
|
||||
showValues?: boolean;
|
||||
}
|
||||
|
||||
function interpolateColor(color1: string, color2: string, factor: number): string {
|
||||
const hex = (c: string) => parseInt(c, 16);
|
||||
const r1 = hex(color1.slice(1, 3));
|
||||
const g1 = hex(color1.slice(3, 5));
|
||||
const b1 = hex(color1.slice(5, 7));
|
||||
const r2 = hex(color2.slice(1, 3));
|
||||
const g2 = hex(color2.slice(3, 5));
|
||||
const b2 = hex(color2.slice(5, 7));
|
||||
|
||||
const r = Math.round(r1 + (r2 - r1) * factor);
|
||||
const g = Math.round(g1 + (g2 - g1) * factor);
|
||||
const b = Math.round(b1 + (b2 - b1) * factor);
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function Heatmap({
|
||||
data,
|
||||
title,
|
||||
xLabels,
|
||||
yLabels,
|
||||
colorRange = { min: '#fee2e2', max: '#10b981' },
|
||||
height = 300,
|
||||
showValues = true,
|
||||
}: HeatmapProps) {
|
||||
const maxValue = Math.max(...data.map((d) => d.value));
|
||||
const minValue = Math.min(...data.map((d) => d.value));
|
||||
const range = maxValue - minValue || 1;
|
||||
|
||||
const getColor = (value: number): string => {
|
||||
const factor = (value - minValue) / range;
|
||||
return interpolateColor(colorRange.min, colorRange.max, factor);
|
||||
};
|
||||
|
||||
const getValue = (x: string, y: string): number | undefined => {
|
||||
const cell = data.find((d) => d.x === x && d.y === y);
|
||||
return cell?.value;
|
||||
};
|
||||
|
||||
const cellWidth = `${100 / xLabels.length}%`;
|
||||
const cellHeight = (height - 40) / yLabels.length;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
||||
<div style={{ height }}>
|
||||
{/* X Labels */}
|
||||
<div className="flex mb-1" style={{ paddingLeft: '80px' }}>
|
||||
{xLabels.map((label) => (
|
||||
<div
|
||||
key={label}
|
||||
className="text-xs text-gray-500 text-center truncate"
|
||||
style={{ width: cellWidth }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{yLabels.map((yLabel) => (
|
||||
<div key={yLabel} className="flex">
|
||||
<div
|
||||
className="flex items-center justify-end pr-2 text-xs text-gray-500"
|
||||
style={{ width: '80px' }}
|
||||
>
|
||||
{yLabel}
|
||||
</div>
|
||||
{xLabels.map((xLabel) => {
|
||||
const value = getValue(xLabel, yLabel);
|
||||
const bgColor = value !== undefined ? getColor(value) : '#f3f4f6';
|
||||
const textColor =
|
||||
value !== undefined && (value - minValue) / range > 0.5
|
||||
? '#fff'
|
||||
: '#374151';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${xLabel}-${yLabel}`}
|
||||
className="flex items-center justify-center border border-white rounded-sm transition-all hover:ring-2 hover:ring-gray-400"
|
||||
style={{
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
backgroundColor: bgColor,
|
||||
}}
|
||||
title={`${xLabel}, ${yLabel}: ${value ?? 'N/A'}`}
|
||||
>
|
||||
{showValues && value !== undefined && (
|
||||
<span className="text-xs font-medium" style={{ color: textColor }}>
|
||||
{value.toFixed(0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex items-center justify-center mt-4 space-x-4">
|
||||
<span className="text-xs text-gray-500">Low</span>
|
||||
<div
|
||||
className="w-24 h-3 rounded"
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${colorRange.min}, ${colorRange.max})`,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">High</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
components/analytics/charts/LineChart.tsx
Normal file
85
components/analytics/charts/LineChart.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* Line Chart Component
|
||||
* Displays time series data as a line chart
|
||||
*/
|
||||
|
||||
import {
|
||||
LineChart as RechartsLineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
interface LineChartProps {
|
||||
data: any[];
|
||||
xKey: string;
|
||||
yKey: string | string[];
|
||||
title?: string;
|
||||
colors?: string[];
|
||||
height?: number;
|
||||
showGrid?: boolean;
|
||||
showLegend?: boolean;
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444'];
|
||||
|
||||
export default function LineChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
title,
|
||||
colors = DEFAULT_COLORS,
|
||||
height = 300,
|
||||
showGrid = true,
|
||||
showLegend = true,
|
||||
formatter = (value) => value.toLocaleString(),
|
||||
}: LineChartProps) {
|
||||
const yKeys = Array.isArray(yKey) ? yKey : [yKey];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RechartsLineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />}
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
tick={{ fill: '#6b7280', fontSize: 12 }}
|
||||
tickLine={{ stroke: '#e5e7eb' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#6b7280', fontSize: 12 }}
|
||||
tickLine={{ stroke: '#e5e7eb' }}
|
||||
tickFormatter={formatter}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
formatter={(value: number) => [formatter(value), '']}
|
||||
/>
|
||||
{showLegend && <Legend />}
|
||||
{yKeys.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index % colors.length]}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: colors[index % colors.length], r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
))}
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
123
components/analytics/charts/PieChart.tsx
Normal file
123
components/analytics/charts/PieChart.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* Pie Chart Component
|
||||
* Displays distribution data as a pie chart
|
||||
*/
|
||||
|
||||
import {
|
||||
PieChart as RechartsPieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
interface PieChartProps {
|
||||
data: any[];
|
||||
dataKey: string;
|
||||
nameKey: string;
|
||||
title?: string;
|
||||
colors?: string[];
|
||||
height?: number;
|
||||
showLegend?: boolean;
|
||||
innerRadius?: number;
|
||||
outerRadius?: number;
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
'#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444',
|
||||
'#06b6d4', '#ec4899', '#84cc16', '#f97316', '#6366f1',
|
||||
];
|
||||
|
||||
export default function PieChart({
|
||||
data,
|
||||
dataKey,
|
||||
nameKey,
|
||||
title,
|
||||
colors = DEFAULT_COLORS,
|
||||
height = 300,
|
||||
showLegend = true,
|
||||
innerRadius = 0,
|
||||
outerRadius = 80,
|
||||
formatter = (value) => value.toLocaleString(),
|
||||
}: PieChartProps) {
|
||||
const RADIAN = Math.PI / 180;
|
||||
|
||||
const renderCustomizedLabel = ({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
percent,
|
||||
}: any) => {
|
||||
if (percent < 0.05) return null;
|
||||
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill="white"
|
||||
textAnchor={x > cx ? 'start' : 'end'}
|
||||
dominantBaseline="central"
|
||||
fontSize="12"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{`${(percent * 100).toFixed(0)}%`}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RechartsPieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={renderCustomizedLabel}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
paddingAngle={2}
|
||||
dataKey={dataKey}
|
||||
nameKey={nameKey}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={colors[index % colors.length]}
|
||||
stroke="#fff"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
formatter={(value: number) => [formatter(value), '']}
|
||||
/>
|
||||
{showLegend && (
|
||||
<Legend
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
align="center"
|
||||
wrapperStyle={{ paddingTop: '20px' }}
|
||||
/>
|
||||
)}
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
components/analytics/charts/index.ts
Normal file
11
components/analytics/charts/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Chart Components Index
|
||||
* Export all chart components
|
||||
*/
|
||||
|
||||
export { default as LineChart } from './LineChart';
|
||||
export { default as BarChart } from './BarChart';
|
||||
export { default as PieChart } from './PieChart';
|
||||
export { default as AreaChart } from './AreaChart';
|
||||
export { default as Gauge } from './Gauge';
|
||||
export { default as Heatmap } from './Heatmap';
|
||||
|
|
@ -1,3 +1,19 @@
|
|||
/**
|
||||
* Analytics Components Index
|
||||
* Export all analytics components
|
||||
*/
|
||||
|
||||
// Charts
|
||||
export * from './charts';
|
||||
|
||||
// Widgets
|
||||
export { default as KPICard } from './KPICard';
|
||||
export { default as TrendIndicator } from './TrendIndicator';
|
||||
export { default as DataTable } from './DataTable';
|
||||
export { default as DateRangePicker } from './DateRangePicker';
|
||||
export { default as FilterPanel } from './FilterPanel';
|
||||
|
||||
// Existing components
|
||||
export { default as EnvironmentalImpact } from './EnvironmentalImpact';
|
||||
export { default as FoodMilesTracker } from './FoodMilesTracker';
|
||||
export { default as SavingsCalculator } from './SavingsCalculator';
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export class AgentOrchestrator {
|
|||
}
|
||||
|
||||
// Stop all agents
|
||||
for (const agent of this.agents.values()) {
|
||||
for (const agent of Array.from(this.agents.values())) {
|
||||
try {
|
||||
await agent.stop();
|
||||
console.log(`[Orchestrator] Stopped: ${agent.config.name}`);
|
||||
|
|
@ -275,7 +275,7 @@ export class AgentOrchestrator {
|
|||
* Perform health check on all agents
|
||||
*/
|
||||
private performHealthCheck(): void {
|
||||
for (const [agentId, agent] of this.agents) {
|
||||
for (const [agentId, agent] of Array.from(this.agents.entries())) {
|
||||
const health = this.getAgentHealth(agentId);
|
||||
|
||||
if (!health.isHealthy) {
|
||||
|
|
@ -296,7 +296,7 @@ export class AgentOrchestrator {
|
|||
private aggregateAlerts(): void {
|
||||
this.aggregatedAlerts = [];
|
||||
|
||||
for (const agent of this.agents.values()) {
|
||||
for (const agent of Array.from(this.agents.values())) {
|
||||
const alerts = agent.getAlerts()
|
||||
.filter(a => !a.acknowledged)
|
||||
.slice(-this.config.maxAlertsPerAgent);
|
||||
|
|
|
|||
406
lib/analytics/aggregator.ts
Normal file
406
lib/analytics/aggregator.ts
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
/**
|
||||
* Data Aggregator for Analytics
|
||||
* Aggregates data from various sources for analytics dashboards
|
||||
*/
|
||||
|
||||
import {
|
||||
AnalyticsOverview,
|
||||
PlantAnalytics,
|
||||
TransportAnalytics,
|
||||
FarmAnalytics,
|
||||
SustainabilityAnalytics,
|
||||
TimeRange,
|
||||
DateRange,
|
||||
TimeSeriesDataPoint,
|
||||
AnalyticsFilters,
|
||||
AggregationConfig,
|
||||
GroupByPeriod,
|
||||
} from './types';
|
||||
import { subDays, subMonths, startOfDay, endOfDay, format, eachDayOfInterval, parseISO } from 'date-fns';
|
||||
|
||||
// Mock data generators for demonstration - in production these would query actual databases
|
||||
|
||||
/**
|
||||
* Get date range from TimeRange enum
|
||||
*/
|
||||
export function getDateRangeFromTimeRange(timeRange: TimeRange): DateRange {
|
||||
const end = endOfDay(new Date());
|
||||
let start: Date;
|
||||
|
||||
switch (timeRange) {
|
||||
case '7d':
|
||||
start = startOfDay(subDays(new Date(), 7));
|
||||
break;
|
||||
case '30d':
|
||||
start = startOfDay(subDays(new Date(), 30));
|
||||
break;
|
||||
case '90d':
|
||||
start = startOfDay(subDays(new Date(), 90));
|
||||
break;
|
||||
case '365d':
|
||||
start = startOfDay(subDays(new Date(), 365));
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
start = startOfDay(subMonths(new Date(), 24)); // Default to 2 years
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate time series data points for a date range
|
||||
*/
|
||||
export function generateTimeSeriesPoints(
|
||||
dateRange: DateRange,
|
||||
valueGenerator: (date: Date, index: number) => number
|
||||
): TimeSeriesDataPoint[] {
|
||||
const days = eachDayOfInterval({ start: dateRange.start, end: dateRange.end });
|
||||
return days.map((day, index) => ({
|
||||
timestamp: format(day, 'yyyy-MM-dd'),
|
||||
value: valueGenerator(day, index),
|
||||
label: format(day, 'MMM d'),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate data by time period
|
||||
*/
|
||||
export function aggregateByPeriod<T>(
|
||||
data: T[],
|
||||
dateField: keyof T,
|
||||
valueField: keyof T,
|
||||
period: GroupByPeriod
|
||||
): Record<string, number> {
|
||||
const aggregated: Record<string, number> = {};
|
||||
|
||||
data.forEach((item) => {
|
||||
const date = parseISO(item[dateField] as string);
|
||||
let key: string;
|
||||
|
||||
switch (period) {
|
||||
case 'hour':
|
||||
key = format(date, 'yyyy-MM-dd HH:00');
|
||||
break;
|
||||
case 'day':
|
||||
key = format(date, 'yyyy-MM-dd');
|
||||
break;
|
||||
case 'week':
|
||||
key = format(date, "yyyy-'W'ww");
|
||||
break;
|
||||
case 'month':
|
||||
key = format(date, 'yyyy-MM');
|
||||
break;
|
||||
case 'year':
|
||||
key = format(date, 'yyyy');
|
||||
break;
|
||||
}
|
||||
|
||||
aggregated[key] = (aggregated[key] || 0) + (item[valueField] as number);
|
||||
});
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage change between two values
|
||||
*/
|
||||
export function calculateChange(current: number, previous: number): { change: number; percent: number } {
|
||||
const change = current - previous;
|
||||
const percent = previous !== 0 ? (change / previous) * 100 : current > 0 ? 100 : 0;
|
||||
return { change, percent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics overview with aggregated metrics
|
||||
*/
|
||||
export async function getAnalyticsOverview(
|
||||
filters: AnalyticsFilters = { timeRange: '30d' }
|
||||
): Promise<AnalyticsOverview> {
|
||||
const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange);
|
||||
|
||||
// In production, these would be actual database queries
|
||||
// For now, generate realistic mock data
|
||||
const baseValue = 1000 + Math.random() * 500;
|
||||
|
||||
return {
|
||||
totalPlants: Math.floor(baseValue * 1.5),
|
||||
plantsRegisteredToday: Math.floor(Math.random() * 15 + 5),
|
||||
plantsRegisteredThisWeek: Math.floor(Math.random() * 80 + 40),
|
||||
plantsRegisteredThisMonth: Math.floor(Math.random() * 250 + 150),
|
||||
totalTransportEvents: Math.floor(baseValue * 2.3),
|
||||
totalCarbonKg: Math.round((Math.random() * 500 + 200) * 100) / 100,
|
||||
totalFoodMiles: Math.round((Math.random() * 10000 + 5000) * 10) / 10,
|
||||
activeUsers: Math.floor(Math.random() * 200 + 100),
|
||||
growthRate: Math.round((Math.random() * 20 + 5) * 10) / 10,
|
||||
trendsData: [
|
||||
{
|
||||
metric: 'Plants',
|
||||
currentValue: Math.floor(baseValue * 1.5),
|
||||
previousValue: Math.floor(baseValue * 1.35),
|
||||
change: Math.floor(baseValue * 0.15),
|
||||
changePercent: 11.1,
|
||||
direction: 'up',
|
||||
period: filters.timeRange,
|
||||
},
|
||||
{
|
||||
metric: 'Carbon Saved',
|
||||
currentValue: Math.round((Math.random() * 200 + 100) * 10) / 10,
|
||||
previousValue: Math.round((Math.random() * 180 + 90) * 10) / 10,
|
||||
change: Math.round((Math.random() * 20 + 10) * 10) / 10,
|
||||
changePercent: 12.5,
|
||||
direction: 'up',
|
||||
period: filters.timeRange,
|
||||
},
|
||||
{
|
||||
metric: 'Active Users',
|
||||
currentValue: Math.floor(Math.random() * 200 + 100),
|
||||
previousValue: Math.floor(Math.random() * 180 + 90),
|
||||
change: Math.floor(Math.random() * 30 + 10),
|
||||
changePercent: 8.3,
|
||||
direction: 'up',
|
||||
period: filters.timeRange,
|
||||
},
|
||||
{
|
||||
metric: 'Food Miles',
|
||||
currentValue: Math.round((Math.random() * 5000 + 2500) * 10) / 10,
|
||||
previousValue: Math.round((Math.random() * 5500 + 2800) * 10) / 10,
|
||||
change: -Math.round((Math.random() * 500 + 200) * 10) / 10,
|
||||
changePercent: -8.7,
|
||||
direction: 'down',
|
||||
period: filters.timeRange,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plant-specific analytics
|
||||
*/
|
||||
export async function getPlantAnalytics(
|
||||
filters: AnalyticsFilters = { timeRange: '30d' }
|
||||
): Promise<PlantAnalytics> {
|
||||
const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange);
|
||||
|
||||
const speciesData = [
|
||||
{ species: 'Tomato', count: 245, percentage: 28.5, trend: 'up' as const },
|
||||
{ species: 'Lettuce', count: 198, percentage: 23.0, trend: 'up' as const },
|
||||
{ species: 'Pepper', count: 156, percentage: 18.1, trend: 'stable' as const },
|
||||
{ species: 'Basil', count: 134, percentage: 15.6, trend: 'up' as const },
|
||||
{ species: 'Cucumber', count: 87, percentage: 10.1, trend: 'down' as const },
|
||||
{ species: 'Other', count: 41, percentage: 4.7, trend: 'stable' as const },
|
||||
];
|
||||
|
||||
return {
|
||||
totalPlants: speciesData.reduce((sum, s) => sum + s.count, 0),
|
||||
plantsBySpecies: speciesData,
|
||||
plantsByGeneration: [
|
||||
{ generation: 1, count: 340, percentage: 39.5 },
|
||||
{ generation: 2, count: 280, percentage: 32.5 },
|
||||
{ generation: 3, count: 156, percentage: 18.1 },
|
||||
{ generation: 4, count: 68, percentage: 7.9 },
|
||||
{ generation: 5, count: 17, percentage: 2.0 },
|
||||
],
|
||||
registrationsTrend: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.floor(Math.random() * 15 + 5 + Math.sin(i / 7) * 5)
|
||||
),
|
||||
averageLineageDepth: 2.3,
|
||||
topGrowers: [
|
||||
{ userId: 'user-1', name: 'Green Gardens Co', totalPlants: 145, totalSpecies: 12, averageGeneration: 2.1 },
|
||||
{ userId: 'user-2', name: 'Urban Farm LLC', totalPlants: 98, totalSpecies: 8, averageGeneration: 1.8 },
|
||||
{ userId: 'user-3', name: 'Local Seeds Inc', totalPlants: 76, totalSpecies: 15, averageGeneration: 3.2 },
|
||||
],
|
||||
recentRegistrations: [
|
||||
{ id: 'plant-1', name: 'Cherry Tomato #245', species: 'Tomato', registeredAt: new Date().toISOString(), generation: 3 },
|
||||
{ id: 'plant-2', name: 'Butterhead Lettuce', species: 'Lettuce', registeredAt: new Date().toISOString(), generation: 2 },
|
||||
{ id: 'plant-3', name: 'Sweet Basil', species: 'Basil', registeredAt: new Date().toISOString(), generation: 1 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transport analytics
|
||||
*/
|
||||
export async function getTransportAnalytics(
|
||||
filters: AnalyticsFilters = { timeRange: '30d' }
|
||||
): Promise<TransportAnalytics> {
|
||||
const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange);
|
||||
|
||||
return {
|
||||
totalEvents: 2847,
|
||||
totalDistanceKm: 15234.5,
|
||||
totalCarbonKg: 487.3,
|
||||
carbonSavedKg: 1256.8,
|
||||
eventsByType: [
|
||||
{ eventType: 'seed_acquisition', count: 423, percentage: 14.9, carbonKg: 52.3 },
|
||||
{ eventType: 'growing_transport', count: 687, percentage: 24.1, carbonKg: 112.4 },
|
||||
{ eventType: 'harvest', count: 534, percentage: 18.8, carbonKg: 45.2 },
|
||||
{ eventType: 'distribution', count: 756, percentage: 26.6, carbonKg: 178.9 },
|
||||
{ eventType: 'consumer_delivery', count: 447, percentage: 15.7, carbonKg: 98.5 },
|
||||
],
|
||||
eventsByMethod: [
|
||||
{ method: 'walking', count: 312, percentage: 11.0, distanceKm: 156, carbonKg: 0, efficiency: 100 },
|
||||
{ method: 'bicycle', count: 534, percentage: 18.8, distanceKm: 1602, carbonKg: 0, efficiency: 100 },
|
||||
{ method: 'electric_vehicle', count: 687, percentage: 24.1, distanceKm: 4806, carbonKg: 72.1, efficiency: 85 },
|
||||
{ method: 'gasoline_vehicle', count: 756, percentage: 26.6, distanceKm: 5292, carbonKg: 264.6, efficiency: 45 },
|
||||
{ method: 'local_delivery', count: 558, percentage: 19.6, distanceKm: 3378, carbonKg: 150.6, efficiency: 60 },
|
||||
],
|
||||
dailyStats: generateTimeSeriesPoints(dateRange, (_, i) => ({
|
||||
date: format(dateRange.start, 'yyyy-MM-dd'),
|
||||
eventCount: Math.floor(Math.random() * 80 + 40),
|
||||
distanceKm: Math.round((Math.random() * 500 + 200) * 10) / 10,
|
||||
carbonKg: Math.round((Math.random() * 20 + 5) * 100) / 100,
|
||||
})).map(p => ({
|
||||
date: p.timestamp,
|
||||
eventCount: p.value,
|
||||
distanceKm: Math.round((Math.random() * 500 + 200) * 10) / 10,
|
||||
carbonKg: Math.round((Math.random() * 20 + 5) * 100) / 100,
|
||||
})),
|
||||
averageDistancePerEvent: 5.35,
|
||||
mostEfficientRoutes: [
|
||||
{ from: 'Local Farm A', to: 'Community Center', method: 'bicycle', distanceKm: 2.3, carbonKg: 0, frequency: 45 },
|
||||
{ from: 'Urban Garden', to: 'Farmers Market', method: 'walking', distanceKm: 0.8, carbonKg: 0, frequency: 38 },
|
||||
{ from: 'Rooftop Farm', to: 'Restaurant Row', method: 'electric_vehicle', distanceKm: 4.5, carbonKg: 0.07, frequency: 32 },
|
||||
],
|
||||
carbonTrend: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.round((Math.random() * 15 + 10 - i * 0.1) * 100) / 100
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get farm analytics
|
||||
*/
|
||||
export async function getFarmAnalytics(
|
||||
filters: AnalyticsFilters = { timeRange: '30d' }
|
||||
): Promise<FarmAnalytics> {
|
||||
const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange);
|
||||
|
||||
return {
|
||||
totalFarms: 24,
|
||||
totalZones: 156,
|
||||
activeBatches: 89,
|
||||
completedBatches: 234,
|
||||
averageYieldKg: 45.6,
|
||||
resourceUsage: {
|
||||
waterLiters: 125000,
|
||||
energyKwh: 8500,
|
||||
nutrientsKg: 450,
|
||||
waterEfficiency: 87.5,
|
||||
energyEfficiency: 92.3,
|
||||
},
|
||||
performanceByZone: [
|
||||
{ zoneId: 'zone-1', zoneName: 'Zone A - Leafy Greens', currentCrop: 'Lettuce', healthScore: 94, yieldKg: 52.3, efficiency: 91 },
|
||||
{ zoneId: 'zone-2', zoneName: 'Zone B - Herbs', currentCrop: 'Basil', healthScore: 88, yieldKg: 38.7, efficiency: 85 },
|
||||
{ zoneId: 'zone-3', zoneName: 'Zone C - Tomatoes', currentCrop: 'Cherry Tomato', healthScore: 92, yieldKg: 67.4, efficiency: 89 },
|
||||
{ zoneId: 'zone-4', zoneName: 'Zone D - Microgreens', currentCrop: 'Mixed Micro', healthScore: 96, yieldKg: 24.1, efficiency: 94 },
|
||||
],
|
||||
batchCompletionTrend: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.floor(Math.random() * 5 + 2)
|
||||
),
|
||||
yieldPredictions: [
|
||||
{ cropType: 'Lettuce', predictedYieldKg: 156.5, confidence: 0.92, harvestDate: format(subDays(new Date(), -7), 'yyyy-MM-dd') },
|
||||
{ cropType: 'Tomato', predictedYieldKg: 234.8, confidence: 0.87, harvestDate: format(subDays(new Date(), -14), 'yyyy-MM-dd') },
|
||||
{ cropType: 'Basil', predictedYieldKg: 45.2, confidence: 0.94, harvestDate: format(subDays(new Date(), -5), 'yyyy-MM-dd') },
|
||||
],
|
||||
topPerformingCrops: [
|
||||
{ cropType: 'Lettuce', averageYieldKg: 48.3, growthDays: 28, successRate: 94.5, batches: 45 },
|
||||
{ cropType: 'Basil', averageYieldKg: 12.4, growthDays: 21, successRate: 91.2, batches: 38 },
|
||||
{ cropType: 'Cherry Tomato', averageYieldKg: 67.8, growthDays: 65, successRate: 88.7, batches: 22 },
|
||||
{ cropType: 'Microgreens', averageYieldKg: 5.6, growthDays: 14, successRate: 96.8, batches: 67 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sustainability analytics
|
||||
*/
|
||||
export async function getSustainabilityAnalytics(
|
||||
filters: AnalyticsFilters = { timeRange: '30d' }
|
||||
): Promise<SustainabilityAnalytics> {
|
||||
const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange);
|
||||
|
||||
return {
|
||||
overallScore: 82.5,
|
||||
carbonFootprint: {
|
||||
totalEmittedKg: 487.3,
|
||||
totalSavedKg: 1256.8,
|
||||
netImpactKg: -769.5,
|
||||
reductionPercentage: 72.1,
|
||||
equivalentTrees: 38.4,
|
||||
monthlyTrend: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.round((50 - i * 0.5 + Math.random() * 10) * 10) / 10
|
||||
),
|
||||
},
|
||||
foodMiles: {
|
||||
totalMiles: 15234.5,
|
||||
averageMilesPerPlant: 17.7,
|
||||
savedMiles: 48672.3,
|
||||
localPercentage: 76.2,
|
||||
monthlyTrend: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.round((600 - i * 5 + Math.random() * 100) * 10) / 10
|
||||
),
|
||||
},
|
||||
waterUsage: {
|
||||
totalUsedLiters: 125000,
|
||||
savedLiters: 87500,
|
||||
efficiencyScore: 87.5,
|
||||
perKgProduce: 2.8,
|
||||
},
|
||||
localProduction: {
|
||||
localCount: 654,
|
||||
totalCount: 861,
|
||||
percentage: 76.0,
|
||||
trend: 'up',
|
||||
},
|
||||
goals: [
|
||||
{ id: 'goal-1', name: 'Carbon Neutral by 2025', target: 0, current: 487.3, unit: 'kg CO2', progress: 72, deadline: '2025-12-31', status: 'on_track' },
|
||||
{ id: 'goal-2', name: '80% Local Production', target: 80, current: 76, unit: '%', progress: 95, deadline: '2024-12-31', status: 'on_track' },
|
||||
{ id: 'goal-3', name: 'Reduce Food Miles 50%', target: 50, current: 38, unit: '%', progress: 76, deadline: '2024-06-30', status: 'at_risk' },
|
||||
{ id: 'goal-4', name: 'Water Efficiency 90%', target: 90, current: 87.5, unit: '%', progress: 97, deadline: '2024-12-31', status: 'on_track' },
|
||||
],
|
||||
trends: [
|
||||
{
|
||||
metric: 'Carbon Reduction',
|
||||
values: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.round((65 + i * 0.3 + Math.random() * 5) * 10) / 10
|
||||
),
|
||||
},
|
||||
{
|
||||
metric: 'Local Production',
|
||||
values: generateTimeSeriesPoints(dateRange, (_, i) =>
|
||||
Math.round((70 + i * 0.2 + Math.random() * 3) * 10) / 10
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache management for analytics data
|
||||
*/
|
||||
const analyticsCache = new Map<string, { data: any; timestamp: number }>();
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export function getCachedData<T>(key: string): T | null {
|
||||
const cached = analyticsCache.get(key);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
return cached.data as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setCachedData<T>(key: string, data: T): void {
|
||||
analyticsCache.set(key, { data, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
export function clearCache(): void {
|
||||
analyticsCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key from filters
|
||||
*/
|
||||
export function generateCacheKey(prefix: string, filters: AnalyticsFilters): string {
|
||||
return `${prefix}-${JSON.stringify(filters)}`;
|
||||
}
|
||||
189
lib/analytics/cache.ts
Normal file
189
lib/analytics/cache.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* Cache Management for Analytics
|
||||
* Provides caching for expensive analytics calculations
|
||||
*/
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
class AnalyticsCache {
|
||||
private cache: Map<string, CacheEntry<any>> = new Map();
|
||||
private defaultTTL: number = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Get cached data
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached data
|
||||
*/
|
||||
set<T>(key: string, data: T, ttlMs: number = this.defaultTTL): void {
|
||||
const now = Date.now();
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: now,
|
||||
expiresAt: now + ttlMs,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key exists and is valid
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific key
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
return this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear expired entries
|
||||
*/
|
||||
cleanup(): number {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats(): {
|
||||
size: number;
|
||||
validEntries: number;
|
||||
expiredEntries: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
let valid = 0;
|
||||
let expired = 0;
|
||||
|
||||
for (const entry of this.cache.values()) {
|
||||
if (now > entry.expiresAt) {
|
||||
expired++;
|
||||
} else {
|
||||
valid++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
size: this.cache.size,
|
||||
validEntries: valid,
|
||||
expiredEntries: expired,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key from object
|
||||
*/
|
||||
static generateKey(prefix: string, params: Record<string, any>): string {
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.map((key) => `${key}:${JSON.stringify(params[key])}`)
|
||||
.join('|');
|
||||
return `${prefix}:${sortedParams}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const analyticsCache = new AnalyticsCache();
|
||||
|
||||
/**
|
||||
* Cache decorator for async functions
|
||||
*/
|
||||
export function cached<T>(
|
||||
keyGenerator: (...args: any[]) => string,
|
||||
ttlMs: number = 5 * 60 * 1000
|
||||
) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const cacheKey = keyGenerator(...args);
|
||||
const cached = analyticsCache.get<T>(cacheKey);
|
||||
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await originalMethod.apply(this, args);
|
||||
analyticsCache.set(cacheKey, result, ttlMs);
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order function for caching async functions
|
||||
*/
|
||||
export function withCache<T, A extends any[]>(
|
||||
fn: (...args: A) => Promise<T>,
|
||||
keyGenerator: (...args: A) => string,
|
||||
ttlMs: number = 5 * 60 * 1000
|
||||
): (...args: A) => Promise<T> {
|
||||
return async (...args: A): Promise<T> => {
|
||||
const cacheKey = keyGenerator(...args);
|
||||
const cached = analyticsCache.get<T>(cacheKey);
|
||||
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await fn(...args);
|
||||
analyticsCache.set(cacheKey, result, ttlMs);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// Schedule periodic cleanup
|
||||
if (typeof setInterval !== 'undefined') {
|
||||
setInterval(() => {
|
||||
analyticsCache.cleanup();
|
||||
}, 60 * 1000); // Run cleanup every minute
|
||||
}
|
||||
70
lib/analytics/index.ts
Normal file
70
lib/analytics/index.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Analytics Module Index
|
||||
* Exports all analytics functionality
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Data aggregation
|
||||
export {
|
||||
getDateRangeFromTimeRange,
|
||||
generateTimeSeriesPoints,
|
||||
aggregateByPeriod,
|
||||
calculateChange,
|
||||
getAnalyticsOverview,
|
||||
getPlantAnalytics,
|
||||
getTransportAnalytics,
|
||||
getFarmAnalytics,
|
||||
getSustainabilityAnalytics,
|
||||
getCachedData,
|
||||
setCachedData,
|
||||
clearCache,
|
||||
generateCacheKey,
|
||||
} from './aggregator';
|
||||
|
||||
// Metrics calculations
|
||||
export {
|
||||
mean,
|
||||
median,
|
||||
standardDeviation,
|
||||
percentile,
|
||||
minMax,
|
||||
getTrendDirection,
|
||||
percentageChange,
|
||||
movingAverage,
|
||||
rateOfChange,
|
||||
normalize,
|
||||
cagr,
|
||||
efficiencyScore,
|
||||
carbonIntensity,
|
||||
foodMilesScore,
|
||||
sustainabilityScore,
|
||||
generateKPICards,
|
||||
calculateGrowthMetrics,
|
||||
detectAnomalies,
|
||||
correlationCoefficient,
|
||||
formatNumber,
|
||||
formatPercentage,
|
||||
} from './metrics';
|
||||
|
||||
// Trend analysis
|
||||
export {
|
||||
analyzeTrend,
|
||||
linearRegression,
|
||||
forecast,
|
||||
detectSeasonality,
|
||||
findPeaksAndValleys,
|
||||
calculateMomentum,
|
||||
exponentialSmoothing,
|
||||
generateTrendSummary,
|
||||
compareTimeSeries,
|
||||
getTrendConfidence,
|
||||
yearOverYearComparison,
|
||||
} from './trends';
|
||||
|
||||
// Cache management
|
||||
export {
|
||||
analyticsCache,
|
||||
withCache,
|
||||
} from './cache';
|
||||
326
lib/analytics/metrics.ts
Normal file
326
lib/analytics/metrics.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
/**
|
||||
* Metrics Calculations for Analytics
|
||||
* Provides metric calculations and statistical functions
|
||||
*/
|
||||
|
||||
import { TrendDirection, TimeSeriesDataPoint, KPICardData } from './types';
|
||||
|
||||
/**
|
||||
* Calculate mean of an array of numbers
|
||||
*/
|
||||
export function mean(values: number[]): number {
|
||||
if (values.length === 0) return 0;
|
||||
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate median of an array of numbers
|
||||
*/
|
||||
export function median(values: number[]): number {
|
||||
if (values.length === 0) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate standard deviation
|
||||
*/
|
||||
export function standardDeviation(values: number[]): number {
|
||||
if (values.length === 0) return 0;
|
||||
const avg = mean(values);
|
||||
const squareDiffs = values.map(v => Math.pow(v - avg, 2));
|
||||
return Math.sqrt(mean(squareDiffs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentile
|
||||
*/
|
||||
export function percentile(values: number[], p: number): number {
|
||||
if (values.length === 0) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const index = (p / 100) * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = Math.ceil(index);
|
||||
const weight = index - lower;
|
||||
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate min and max
|
||||
*/
|
||||
export function minMax(values: number[]): { min: number; max: number } {
|
||||
if (values.length === 0) return { min: 0, max: 0 };
|
||||
return {
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine trend direction from two values
|
||||
*/
|
||||
export function getTrendDirection(current: number, previous: number, threshold: number = 0.5): TrendDirection {
|
||||
const change = ((current - previous) / Math.abs(previous || 1)) * 100;
|
||||
if (Math.abs(change) < threshold) return 'stable';
|
||||
return change > 0 ? 'up' : 'down';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage change
|
||||
*/
|
||||
export function percentageChange(current: number, previous: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return ((current - previous) / Math.abs(previous)) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate moving average
|
||||
*/
|
||||
export function movingAverage(data: TimeSeriesDataPoint[], windowSize: number): TimeSeriesDataPoint[] {
|
||||
return data.map((point, index) => {
|
||||
const start = Math.max(0, index - windowSize + 1);
|
||||
const window = data.slice(start, index + 1);
|
||||
const avg = mean(window.map(p => p.value));
|
||||
return {
|
||||
...point,
|
||||
value: Math.round(avg * 100) / 100,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rate of change (derivative)
|
||||
*/
|
||||
export function rateOfChange(data: TimeSeriesDataPoint[]): TimeSeriesDataPoint[] {
|
||||
return data.slice(1).map((point, index) => ({
|
||||
...point,
|
||||
value: point.value - data[index].value,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize values to 0-100 range
|
||||
*/
|
||||
export function normalize(values: number[]): number[] {
|
||||
const { min, max } = minMax(values);
|
||||
const range = max - min;
|
||||
if (range === 0) return values.map(() => 50);
|
||||
return values.map(v => ((v - min) / range) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compound annual growth rate (CAGR)
|
||||
*/
|
||||
export function cagr(startValue: number, endValue: number, years: number): number {
|
||||
if (startValue <= 0 || years <= 0) return 0;
|
||||
return (Math.pow(endValue / startValue, 1 / years) - 1) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate efficiency score
|
||||
*/
|
||||
export function efficiencyScore(actual: number, optimal: number): number {
|
||||
if (optimal === 0) return actual === 0 ? 100 : 0;
|
||||
return Math.min(100, (optimal / actual) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate carbon intensity (kg CO2 per km)
|
||||
*/
|
||||
export function carbonIntensity(carbonKg: number, distanceKm: number): number {
|
||||
if (distanceKm === 0) return 0;
|
||||
return carbonKg / distanceKm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate food miles score (0-100, lower is better)
|
||||
*/
|
||||
export function foodMilesScore(miles: number, maxMiles: number = 5000): number {
|
||||
if (miles >= maxMiles) return 0;
|
||||
return Math.round((1 - miles / maxMiles) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sustainability composite score
|
||||
*/
|
||||
export function sustainabilityScore(
|
||||
carbonReduction: number,
|
||||
localPercentage: number,
|
||||
waterEfficiency: number,
|
||||
wasteReduction: number
|
||||
): number {
|
||||
const weights = {
|
||||
carbon: 0.35,
|
||||
local: 0.25,
|
||||
water: 0.25,
|
||||
waste: 0.15,
|
||||
};
|
||||
|
||||
return Math.round(
|
||||
carbonReduction * weights.carbon +
|
||||
localPercentage * weights.local +
|
||||
waterEfficiency * weights.water +
|
||||
wasteReduction * weights.waste
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate KPI card data from metrics
|
||||
*/
|
||||
export function generateKPICards(metrics: {
|
||||
plants: { current: number; previous: number };
|
||||
carbon: { current: number; previous: number };
|
||||
foodMiles: { current: number; previous: number };
|
||||
users: { current: number; previous: number };
|
||||
sustainability: { current: number; previous: number };
|
||||
}): KPICardData[] {
|
||||
return [
|
||||
{
|
||||
id: 'total-plants',
|
||||
title: 'Total Plants',
|
||||
value: metrics.plants.current,
|
||||
change: metrics.plants.current - metrics.plants.previous,
|
||||
changePercent: percentageChange(metrics.plants.current, metrics.plants.previous),
|
||||
trend: getTrendDirection(metrics.plants.current, metrics.plants.previous),
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'carbon-saved',
|
||||
title: 'Carbon Saved',
|
||||
value: metrics.carbon.current.toFixed(1),
|
||||
unit: 'kg CO2',
|
||||
change: metrics.carbon.current - metrics.carbon.previous,
|
||||
changePercent: percentageChange(metrics.carbon.current, metrics.carbon.previous),
|
||||
trend: getTrendDirection(metrics.carbon.current, metrics.carbon.previous),
|
||||
color: 'teal',
|
||||
},
|
||||
{
|
||||
id: 'food-miles',
|
||||
title: 'Food Miles',
|
||||
value: metrics.foodMiles.current.toFixed(0),
|
||||
unit: 'km',
|
||||
change: metrics.foodMiles.current - metrics.foodMiles.previous,
|
||||
changePercent: percentageChange(metrics.foodMiles.current, metrics.foodMiles.previous),
|
||||
trend: getTrendDirection(metrics.foodMiles.previous, metrics.foodMiles.current), // Inverted: lower is better
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'active-users',
|
||||
title: 'Active Users',
|
||||
value: metrics.users.current,
|
||||
change: metrics.users.current - metrics.users.previous,
|
||||
changePercent: percentageChange(metrics.users.current, metrics.users.previous),
|
||||
trend: getTrendDirection(metrics.users.current, metrics.users.previous),
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
id: 'sustainability',
|
||||
title: 'Sustainability Score',
|
||||
value: metrics.sustainability.current.toFixed(0),
|
||||
unit: '%',
|
||||
change: metrics.sustainability.current - metrics.sustainability.previous,
|
||||
changePercent: percentageChange(metrics.sustainability.current, metrics.sustainability.previous),
|
||||
trend: getTrendDirection(metrics.sustainability.current, metrics.sustainability.previous),
|
||||
color: 'green',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate growth metrics
|
||||
*/
|
||||
export function calculateGrowthMetrics(data: TimeSeriesDataPoint[]): {
|
||||
totalGrowth: number;
|
||||
averageDaily: number;
|
||||
peakValue: number;
|
||||
peakDate: string;
|
||||
trend: TrendDirection;
|
||||
} {
|
||||
if (data.length === 0) {
|
||||
return { totalGrowth: 0, averageDaily: 0, peakValue: 0, peakDate: '', trend: 'stable' };
|
||||
}
|
||||
|
||||
const values = data.map(d => d.value);
|
||||
const total = values.reduce((sum, v) => sum + v, 0);
|
||||
const avgDaily = total / data.length;
|
||||
const maxIndex = values.indexOf(Math.max(...values));
|
||||
|
||||
const firstHalf = mean(values.slice(0, Math.floor(values.length / 2)));
|
||||
const secondHalf = mean(values.slice(Math.floor(values.length / 2)));
|
||||
|
||||
return {
|
||||
totalGrowth: total,
|
||||
averageDaily: Math.round(avgDaily * 100) / 100,
|
||||
peakValue: values[maxIndex],
|
||||
peakDate: data[maxIndex].timestamp,
|
||||
trend: getTrendDirection(secondHalf, firstHalf),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect anomalies using z-score
|
||||
*/
|
||||
export function detectAnomalies(
|
||||
data: TimeSeriesDataPoint[],
|
||||
threshold: number = 2
|
||||
): TimeSeriesDataPoint[] {
|
||||
const values = data.map(d => d.value);
|
||||
const avg = mean(values);
|
||||
const std = standardDeviation(values);
|
||||
|
||||
if (std === 0) return [];
|
||||
|
||||
return data.filter(point => {
|
||||
const zScore = Math.abs((point.value - avg) / std);
|
||||
return zScore > threshold;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate correlation coefficient between two datasets
|
||||
*/
|
||||
export function correlationCoefficient(x: number[], y: number[]): number {
|
||||
if (x.length !== y.length || x.length === 0) return 0;
|
||||
|
||||
const n = x.length;
|
||||
const meanX = mean(x);
|
||||
const meanY = mean(y);
|
||||
|
||||
let numerator = 0;
|
||||
let denomX = 0;
|
||||
let denomY = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dx = x[i] - meanX;
|
||||
const dy = y[i] - meanY;
|
||||
numerator += dx * dy;
|
||||
denomX += dx * dx;
|
||||
denomY += dy * dy;
|
||||
}
|
||||
|
||||
const denominator = Math.sqrt(denomX * denomY);
|
||||
return denominator === 0 ? 0 : numerator / denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format large numbers for display
|
||||
*/
|
||||
export function formatNumber(value: number, decimals: number = 1): string {
|
||||
if (Math.abs(value) >= 1000000) {
|
||||
return (value / 1000000).toFixed(decimals) + 'M';
|
||||
}
|
||||
if (Math.abs(value) >= 1000) {
|
||||
return (value / 1000).toFixed(decimals) + 'K';
|
||||
}
|
||||
return value.toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage for display
|
||||
*/
|
||||
export function formatPercentage(value: number, showSign: boolean = false): string {
|
||||
const formatted = value.toFixed(1);
|
||||
if (showSign && value > 0) return '+' + formatted + '%';
|
||||
return formatted + '%';
|
||||
}
|
||||
411
lib/analytics/trends.ts
Normal file
411
lib/analytics/trends.ts
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/**
|
||||
* Trend Analysis for Analytics
|
||||
* Provides trend detection, forecasting, and pattern analysis
|
||||
*/
|
||||
|
||||
import { TimeSeriesDataPoint, TrendDirection, TrendData, TimeRange } from './types';
|
||||
import { mean, standardDeviation, percentageChange, movingAverage } from './metrics';
|
||||
import { format, parseISO, differenceInDays } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Analyze trend from time series data
|
||||
*/
|
||||
export function analyzeTrend(data: TimeSeriesDataPoint[]): TrendData {
|
||||
if (data.length < 2) {
|
||||
return {
|
||||
metric: 'Unknown',
|
||||
currentValue: data[0]?.value || 0,
|
||||
previousValue: 0,
|
||||
change: 0,
|
||||
changePercent: 0,
|
||||
direction: 'stable',
|
||||
period: 'N/A',
|
||||
};
|
||||
}
|
||||
|
||||
const midpoint = Math.floor(data.length / 2);
|
||||
const firstHalf = data.slice(0, midpoint);
|
||||
const secondHalf = data.slice(midpoint);
|
||||
|
||||
const firstAvg = mean(firstHalf.map(d => d.value));
|
||||
const secondAvg = mean(secondHalf.map(d => d.value));
|
||||
const change = secondAvg - firstAvg;
|
||||
const changePercent = percentageChange(secondAvg, firstAvg);
|
||||
|
||||
let direction: TrendDirection = 'stable';
|
||||
if (changePercent > 5) direction = 'up';
|
||||
else if (changePercent < -5) direction = 'down';
|
||||
|
||||
return {
|
||||
metric: '',
|
||||
currentValue: secondAvg,
|
||||
previousValue: firstAvg,
|
||||
change,
|
||||
changePercent: Math.round(changePercent * 10) / 10,
|
||||
direction,
|
||||
period: `${data[0].timestamp} - ${data[data.length - 1].timestamp}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate linear regression for forecasting
|
||||
*/
|
||||
export function linearRegression(data: TimeSeriesDataPoint[]): {
|
||||
slope: number;
|
||||
intercept: number;
|
||||
rSquared: number;
|
||||
} {
|
||||
const n = data.length;
|
||||
if (n < 2) return { slope: 0, intercept: 0, rSquared: 0 };
|
||||
|
||||
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
||||
|
||||
data.forEach((point, i) => {
|
||||
const x = i;
|
||||
const y = point.value;
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
sumXY += x * y;
|
||||
sumX2 += x * x;
|
||||
sumY2 += y * y;
|
||||
});
|
||||
|
||||
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
|
||||
// Calculate R-squared
|
||||
const yMean = sumY / n;
|
||||
let ssRes = 0, ssTot = 0;
|
||||
data.forEach((point, i) => {
|
||||
const predicted = slope * i + intercept;
|
||||
ssRes += Math.pow(point.value - predicted, 2);
|
||||
ssTot += Math.pow(point.value - yMean, 2);
|
||||
});
|
||||
const rSquared = ssTot === 0 ? 1 : 1 - ssRes / ssTot;
|
||||
|
||||
return {
|
||||
slope: Math.round(slope * 1000) / 1000,
|
||||
intercept: Math.round(intercept * 1000) / 1000,
|
||||
rSquared: Math.round(rSquared * 1000) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Forecast future values using linear regression
|
||||
*/
|
||||
export function forecast(
|
||||
data: TimeSeriesDataPoint[],
|
||||
periodsAhead: number
|
||||
): TimeSeriesDataPoint[] {
|
||||
const regression = linearRegression(data);
|
||||
const predictions: TimeSeriesDataPoint[] = [];
|
||||
const lastIndex = data.length - 1;
|
||||
|
||||
for (let i = 1; i <= periodsAhead; i++) {
|
||||
const predictedValue = regression.slope * (lastIndex + i) + regression.intercept;
|
||||
predictions.push({
|
||||
timestamp: `+${i}`,
|
||||
value: Math.max(0, Math.round(predictedValue * 100) / 100),
|
||||
label: `Forecast ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
return predictions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect seasonality in data
|
||||
*/
|
||||
export function detectSeasonality(data: TimeSeriesDataPoint[]): {
|
||||
hasSeasonality: boolean;
|
||||
period: number;
|
||||
strength: number;
|
||||
} {
|
||||
if (data.length < 14) {
|
||||
return { hasSeasonality: false, period: 0, strength: 0 };
|
||||
}
|
||||
|
||||
// Try common periods: 7 days (weekly), 30 days (monthly)
|
||||
const periods = [7, 14, 30];
|
||||
let bestPeriod = 0;
|
||||
let bestCorrelation = 0;
|
||||
|
||||
for (const period of periods) {
|
||||
if (data.length < period * 2) continue;
|
||||
|
||||
let correlation = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let i = period; i < data.length; i++) {
|
||||
correlation += Math.abs(data[i].value - data[i - period].value);
|
||||
count++;
|
||||
}
|
||||
|
||||
const avgCorr = count > 0 ? 1 - correlation / (count * mean(data.map(d => d.value))) : 0;
|
||||
if (avgCorr > bestCorrelation) {
|
||||
bestCorrelation = avgCorr;
|
||||
bestPeriod = period;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasSeasonality: bestCorrelation > 0.5,
|
||||
period: bestPeriod,
|
||||
strength: Math.round(bestCorrelation * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify peaks and valleys in data
|
||||
*/
|
||||
export function findPeaksAndValleys(
|
||||
data: TimeSeriesDataPoint[],
|
||||
sensitivity: number = 1.5
|
||||
): {
|
||||
peaks: TimeSeriesDataPoint[];
|
||||
valleys: TimeSeriesDataPoint[];
|
||||
} {
|
||||
const peaks: TimeSeriesDataPoint[] = [];
|
||||
const valleys: TimeSeriesDataPoint[] = [];
|
||||
|
||||
if (data.length < 3) return { peaks, valleys };
|
||||
|
||||
const avg = mean(data.map(d => d.value));
|
||||
const std = standardDeviation(data.map(d => d.value));
|
||||
const threshold = std * sensitivity;
|
||||
|
||||
for (let i = 1; i < data.length - 1; i++) {
|
||||
const prev = data[i - 1].value;
|
||||
const curr = data[i].value;
|
||||
const next = data[i + 1].value;
|
||||
|
||||
// Peak detection
|
||||
if (curr > prev && curr > next && curr > avg + threshold) {
|
||||
peaks.push(data[i]);
|
||||
}
|
||||
|
||||
// Valley detection
|
||||
if (curr < prev && curr < next && curr < avg - threshold) {
|
||||
valleys.push(data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return { peaks, valleys };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate trend momentum
|
||||
*/
|
||||
export function calculateMomentum(data: TimeSeriesDataPoint[], lookback: number = 5): number {
|
||||
if (data.length < lookback) return 0;
|
||||
|
||||
const recent = data.slice(-lookback);
|
||||
const older = data.slice(-lookback * 2, -lookback);
|
||||
|
||||
if (older.length === 0) return 0;
|
||||
|
||||
const recentAvg = mean(recent.map(d => d.value));
|
||||
const olderAvg = mean(older.map(d => d.value));
|
||||
|
||||
return Math.round(((recentAvg - olderAvg) / olderAvg) * 100 * 10) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth data using exponential smoothing
|
||||
*/
|
||||
export function exponentialSmoothing(
|
||||
data: TimeSeriesDataPoint[],
|
||||
alpha: number = 0.3
|
||||
): TimeSeriesDataPoint[] {
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const smoothed: TimeSeriesDataPoint[] = [data[0]];
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const smoothedValue = alpha * data[i].value + (1 - alpha) * smoothed[i - 1].value;
|
||||
smoothed.push({
|
||||
...data[i],
|
||||
value: Math.round(smoothedValue * 100) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate trend summary text
|
||||
*/
|
||||
export function generateTrendSummary(data: TimeSeriesDataPoint[], metricName: string): string {
|
||||
if (data.length < 2) return `Insufficient data for ${metricName} analysis.`;
|
||||
|
||||
const trend = analyzeTrend(data);
|
||||
const regression = linearRegression(data);
|
||||
const { peaks, valleys } = findPeaksAndValleys(data);
|
||||
const momentum = calculateMomentum(data);
|
||||
|
||||
let summary = '';
|
||||
|
||||
// Overall direction
|
||||
if (trend.direction === 'up') {
|
||||
summary += `${metricName} is trending upward with a ${Math.abs(trend.changePercent).toFixed(1)}% increase. `;
|
||||
} else if (trend.direction === 'down') {
|
||||
summary += `${metricName} is trending downward with a ${Math.abs(trend.changePercent).toFixed(1)}% decrease. `;
|
||||
} else {
|
||||
summary += `${metricName} remains relatively stable. `;
|
||||
}
|
||||
|
||||
// Trend strength
|
||||
if (regression.rSquared > 0.8) {
|
||||
summary += 'The trend is strong and consistent. ';
|
||||
} else if (regression.rSquared > 0.5) {
|
||||
summary += 'The trend is moderate with some variability. ';
|
||||
} else {
|
||||
summary += 'Data shows high variability. ';
|
||||
}
|
||||
|
||||
// Momentum
|
||||
if (momentum > 10) {
|
||||
summary += 'Recent acceleration detected. ';
|
||||
} else if (momentum < -10) {
|
||||
summary += 'Recent deceleration detected. ';
|
||||
}
|
||||
|
||||
// Notable events
|
||||
if (peaks.length > 0) {
|
||||
summary += `${peaks.length} notable peak(s) observed. `;
|
||||
}
|
||||
if (valleys.length > 0) {
|
||||
summary += `${valleys.length} notable dip(s) observed. `;
|
||||
}
|
||||
|
||||
return summary.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two time series for similarity
|
||||
*/
|
||||
export function compareTimeSeries(
|
||||
series1: TimeSeriesDataPoint[],
|
||||
series2: TimeSeriesDataPoint[]
|
||||
): {
|
||||
correlation: number;
|
||||
leadLag: number;
|
||||
divergence: number;
|
||||
} {
|
||||
// Ensure same length
|
||||
const minLength = Math.min(series1.length, series2.length);
|
||||
const s1 = series1.slice(0, minLength).map(d => d.value);
|
||||
const s2 = series2.slice(0, minLength).map(d => d.value);
|
||||
|
||||
// Calculate correlation
|
||||
const mean1 = mean(s1);
|
||||
const mean2 = mean(s2);
|
||||
let numerator = 0, denom1 = 0, denom2 = 0;
|
||||
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
const d1 = s1[i] - mean1;
|
||||
const d2 = s2[i] - mean2;
|
||||
numerator += d1 * d2;
|
||||
denom1 += d1 * d1;
|
||||
denom2 += d2 * d2;
|
||||
}
|
||||
|
||||
const correlation = denom1 * denom2 === 0 ? 0 : numerator / Math.sqrt(denom1 * denom2);
|
||||
|
||||
// Calculate divergence (normalized difference)
|
||||
const divergence = mean(s1.map((v, i) => Math.abs(v - s2[i]))) / ((mean1 + mean2) / 2);
|
||||
|
||||
return {
|
||||
correlation: Math.round(correlation * 1000) / 1000,
|
||||
leadLag: 0, // Simplified - full cross-correlation would require more computation
|
||||
divergence: Math.round(divergence * 1000) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trend confidence level
|
||||
*/
|
||||
export function getTrendConfidence(data: TimeSeriesDataPoint[]): {
|
||||
level: 'high' | 'medium' | 'low';
|
||||
score: number;
|
||||
factors: string[];
|
||||
} {
|
||||
const factors: string[] = [];
|
||||
let score = 50;
|
||||
|
||||
// Data quantity factor
|
||||
if (data.length >= 30) {
|
||||
score += 15;
|
||||
factors.push('Sufficient data points');
|
||||
} else if (data.length >= 14) {
|
||||
score += 10;
|
||||
factors.push('Moderate data points');
|
||||
} else {
|
||||
score -= 10;
|
||||
factors.push('Limited data points');
|
||||
}
|
||||
|
||||
// Consistency factor
|
||||
const std = standardDeviation(data.map(d => d.value));
|
||||
const avg = mean(data.map(d => d.value));
|
||||
const cv = avg !== 0 ? std / avg : 0;
|
||||
|
||||
if (cv < 0.2) {
|
||||
score += 15;
|
||||
factors.push('Low variability');
|
||||
} else if (cv < 0.5) {
|
||||
score += 5;
|
||||
factors.push('Moderate variability');
|
||||
} else {
|
||||
score -= 10;
|
||||
factors.push('High variability');
|
||||
}
|
||||
|
||||
// Trend strength
|
||||
const regression = linearRegression(data);
|
||||
if (regression.rSquared > 0.7) {
|
||||
score += 20;
|
||||
factors.push('Strong trend fit');
|
||||
} else if (regression.rSquared > 0.4) {
|
||||
score += 10;
|
||||
factors.push('Moderate trend fit');
|
||||
} else {
|
||||
score -= 5;
|
||||
factors.push('Weak trend fit');
|
||||
}
|
||||
|
||||
const level = score >= 70 ? 'high' : score >= 50 ? 'medium' : 'low';
|
||||
|
||||
return {
|
||||
level,
|
||||
score: Math.min(100, Math.max(0, score)),
|
||||
factors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate year-over-year comparison
|
||||
*/
|
||||
export function yearOverYearComparison(
|
||||
currentPeriod: TimeSeriesDataPoint[],
|
||||
previousPeriod: TimeSeriesDataPoint[]
|
||||
): {
|
||||
currentTotal: number;
|
||||
previousTotal: number;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
trend: TrendDirection;
|
||||
} {
|
||||
const currentTotal = currentPeriod.reduce((sum, d) => sum + d.value, 0);
|
||||
const previousTotal = previousPeriod.reduce((sum, d) => sum + d.value, 0);
|
||||
const change = currentTotal - previousTotal;
|
||||
const changePercent = percentageChange(currentTotal, previousTotal);
|
||||
|
||||
return {
|
||||
currentTotal: Math.round(currentTotal * 100) / 100,
|
||||
previousTotal: Math.round(previousTotal * 100) / 100,
|
||||
change: Math.round(change * 100) / 100,
|
||||
changePercent: Math.round(changePercent * 10) / 10,
|
||||
trend: changePercent > 5 ? 'up' : changePercent < -5 ? 'down' : 'stable',
|
||||
};
|
||||
}
|
||||
306
lib/analytics/types.ts
Normal file
306
lib/analytics/types.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/**
|
||||
* Analytics Types for LocalGreenChain
|
||||
* Defines all types for the Advanced Analytics Dashboard
|
||||
*/
|
||||
|
||||
// Time range options for analytics queries
|
||||
export type TimeRange = '7d' | '30d' | '90d' | '365d' | 'all';
|
||||
|
||||
export interface DateRange {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
|
||||
// Generic data point for time series
|
||||
export interface TimeSeriesDataPoint {
|
||||
timestamp: string;
|
||||
value: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
// Analytics overview metrics
|
||||
export interface AnalyticsOverview {
|
||||
totalPlants: number;
|
||||
plantsRegisteredToday: number;
|
||||
plantsRegisteredThisWeek: number;
|
||||
plantsRegisteredThisMonth: number;
|
||||
totalTransportEvents: number;
|
||||
totalCarbonKg: number;
|
||||
totalFoodMiles: number;
|
||||
activeUsers: number;
|
||||
growthRate: number;
|
||||
trendsData: TrendData[];
|
||||
}
|
||||
|
||||
// Trend direction indicator
|
||||
export type TrendDirection = 'up' | 'down' | 'stable';
|
||||
|
||||
export interface TrendData {
|
||||
metric: string;
|
||||
currentValue: number;
|
||||
previousValue: number;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
direction: TrendDirection;
|
||||
period: string;
|
||||
}
|
||||
|
||||
// Plant analytics
|
||||
export interface PlantAnalytics {
|
||||
totalPlants: number;
|
||||
plantsBySpecies: SpeciesDistribution[];
|
||||
plantsByGeneration: GenerationDistribution[];
|
||||
registrationsTrend: TimeSeriesDataPoint[];
|
||||
averageLineageDepth: number;
|
||||
topGrowers: GrowerStats[];
|
||||
recentRegistrations: PlantRegistration[];
|
||||
}
|
||||
|
||||
export interface SpeciesDistribution {
|
||||
species: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
trend: TrendDirection;
|
||||
}
|
||||
|
||||
export interface GenerationDistribution {
|
||||
generation: number;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface GrowerStats {
|
||||
userId: string;
|
||||
name: string;
|
||||
totalPlants: number;
|
||||
totalSpecies: number;
|
||||
averageGeneration: number;
|
||||
}
|
||||
|
||||
export interface PlantRegistration {
|
||||
id: string;
|
||||
name: string;
|
||||
species: string;
|
||||
registeredAt: string;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
// Transport analytics
|
||||
export interface TransportAnalytics {
|
||||
totalEvents: number;
|
||||
totalDistanceKm: number;
|
||||
totalCarbonKg: number;
|
||||
carbonSavedKg: number;
|
||||
eventsByType: TransportEventDistribution[];
|
||||
eventsByMethod: TransportMethodDistribution[];
|
||||
dailyStats: DailyTransportStats[];
|
||||
averageDistancePerEvent: number;
|
||||
mostEfficientRoutes: EfficientRoute[];
|
||||
carbonTrend: TimeSeriesDataPoint[];
|
||||
}
|
||||
|
||||
export interface TransportEventDistribution {
|
||||
eventType: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
carbonKg: number;
|
||||
}
|
||||
|
||||
export interface TransportMethodDistribution {
|
||||
method: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
distanceKm: number;
|
||||
carbonKg: number;
|
||||
efficiency: number;
|
||||
}
|
||||
|
||||
export interface DailyTransportStats {
|
||||
date: string;
|
||||
eventCount: number;
|
||||
distanceKm: number;
|
||||
carbonKg: number;
|
||||
}
|
||||
|
||||
export interface EfficientRoute {
|
||||
from: string;
|
||||
to: string;
|
||||
method: string;
|
||||
distanceKm: number;
|
||||
carbonKg: number;
|
||||
frequency: number;
|
||||
}
|
||||
|
||||
// Vertical farm analytics
|
||||
export interface FarmAnalytics {
|
||||
totalFarms: number;
|
||||
totalZones: number;
|
||||
activeBatches: number;
|
||||
completedBatches: number;
|
||||
averageYieldKg: number;
|
||||
resourceUsage: ResourceUsageStats;
|
||||
performanceByZone: ZonePerformance[];
|
||||
batchCompletionTrend: TimeSeriesDataPoint[];
|
||||
yieldPredictions: YieldPrediction[];
|
||||
topPerformingCrops: CropPerformance[];
|
||||
}
|
||||
|
||||
export interface ResourceUsageStats {
|
||||
waterLiters: number;
|
||||
energyKwh: number;
|
||||
nutrientsKg: number;
|
||||
waterEfficiency: number;
|
||||
energyEfficiency: number;
|
||||
}
|
||||
|
||||
export interface ZonePerformance {
|
||||
zoneId: string;
|
||||
zoneName: string;
|
||||
currentCrop: string;
|
||||
healthScore: number;
|
||||
yieldKg: number;
|
||||
efficiency: number;
|
||||
}
|
||||
|
||||
export interface YieldPrediction {
|
||||
cropType: string;
|
||||
predictedYieldKg: number;
|
||||
confidence: number;
|
||||
harvestDate: string;
|
||||
}
|
||||
|
||||
export interface CropPerformance {
|
||||
cropType: string;
|
||||
averageYieldKg: number;
|
||||
growthDays: number;
|
||||
successRate: number;
|
||||
batches: number;
|
||||
}
|
||||
|
||||
// Sustainability analytics
|
||||
export interface SustainabilityAnalytics {
|
||||
overallScore: number;
|
||||
carbonFootprint: CarbonMetrics;
|
||||
foodMiles: FoodMilesMetrics;
|
||||
waterUsage: WaterMetrics;
|
||||
localProduction: LocalProductionMetrics;
|
||||
goals: SustainabilityGoal[];
|
||||
trends: SustainabilityTrend[];
|
||||
}
|
||||
|
||||
export interface CarbonMetrics {
|
||||
totalEmittedKg: number;
|
||||
totalSavedKg: number;
|
||||
netImpactKg: number;
|
||||
reductionPercentage: number;
|
||||
equivalentTrees: number;
|
||||
monthlyTrend: TimeSeriesDataPoint[];
|
||||
}
|
||||
|
||||
export interface FoodMilesMetrics {
|
||||
totalMiles: number;
|
||||
averageMilesPerPlant: number;
|
||||
savedMiles: number;
|
||||
localPercentage: number;
|
||||
monthlyTrend: TimeSeriesDataPoint[];
|
||||
}
|
||||
|
||||
export interface WaterMetrics {
|
||||
totalUsedLiters: number;
|
||||
savedLiters: number;
|
||||
efficiencyScore: number;
|
||||
perKgProduce: number;
|
||||
}
|
||||
|
||||
export interface LocalProductionMetrics {
|
||||
localCount: number;
|
||||
totalCount: number;
|
||||
percentage: number;
|
||||
trend: TrendDirection;
|
||||
}
|
||||
|
||||
export interface SustainabilityGoal {
|
||||
id: string;
|
||||
name: string;
|
||||
target: number;
|
||||
current: number;
|
||||
unit: string;
|
||||
progress: number;
|
||||
deadline: string;
|
||||
status: 'on_track' | 'at_risk' | 'behind';
|
||||
}
|
||||
|
||||
export interface SustainabilityTrend {
|
||||
metric: string;
|
||||
values: TimeSeriesDataPoint[];
|
||||
}
|
||||
|
||||
// Export options
|
||||
export interface ExportOptions {
|
||||
format: 'csv' | 'pdf' | 'json';
|
||||
dateRange: DateRange;
|
||||
sections: string[];
|
||||
includeCharts: boolean;
|
||||
}
|
||||
|
||||
export interface ExportResult {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
data: string;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
// Dashboard configuration
|
||||
export interface DashboardConfig {
|
||||
refreshInterval: number;
|
||||
defaultTimeRange: TimeRange;
|
||||
visibleWidgets: string[];
|
||||
layout: 'grid' | 'list';
|
||||
}
|
||||
|
||||
// KPI Card configuration
|
||||
export interface KPICardData {
|
||||
id: string;
|
||||
title: string;
|
||||
value: number | string;
|
||||
unit?: string;
|
||||
change?: number;
|
||||
changePercent?: number;
|
||||
trend: TrendDirection;
|
||||
color: 'green' | 'blue' | 'purple' | 'orange' | 'red' | 'teal';
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
// Chart configuration
|
||||
export interface ChartConfig {
|
||||
title: string;
|
||||
type: 'line' | 'bar' | 'pie' | 'area' | 'heatmap' | 'gauge';
|
||||
data: any[];
|
||||
xKey?: string;
|
||||
yKey?: string;
|
||||
colors?: string[];
|
||||
showLegend?: boolean;
|
||||
showGrid?: boolean;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// Filter state for analytics
|
||||
export interface AnalyticsFilters {
|
||||
timeRange: TimeRange;
|
||||
dateRange?: DateRange;
|
||||
species?: string[];
|
||||
regions?: string[];
|
||||
transportMethods?: string[];
|
||||
farmIds?: string[];
|
||||
}
|
||||
|
||||
// Aggregation types
|
||||
export type AggregationType = 'sum' | 'avg' | 'min' | 'max' | 'count';
|
||||
export type GroupByPeriod = 'hour' | 'day' | 'week' | 'month' | 'year';
|
||||
|
||||
export interface AggregationConfig {
|
||||
metric: string;
|
||||
aggregation: AggregationType;
|
||||
groupBy?: GroupByPeriod;
|
||||
filters?: Record<string, any>;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ module.exports = {
|
|||
defaultLocale: "en",
|
||||
},
|
||||
images: {
|
||||
domains: [process.env.NEXT_IMAGE_DOMAIN],
|
||||
domains: process.env.NEXT_IMAGE_DOMAIN ? [process.env.NEXT_IMAGE_DOMAIN] : [],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
"@tanstack/react-query": "^4.0.10",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drupal-jsonapi-params": "^1.2.2",
|
||||
"html-react-parser": "^1.2.7",
|
||||
"multer": "^2.0.2",
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.8.6",
|
||||
"recharts": "^3.4.1",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
|
|
@ -60,6 +63,7 @@
|
|||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^17.0.21",
|
||||
|
|
|
|||
253
pages/analytics/farms.tsx
Normal file
253
pages/analytics/farms.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/**
|
||||
* Farm Analytics Page
|
||||
* Vertical farm performance and resource analytics
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
KPICard,
|
||||
DateRangePicker,
|
||||
LineChart,
|
||||
BarChart,
|
||||
Gauge,
|
||||
DataTable,
|
||||
} from '../../components/analytics';
|
||||
import { TimeRange, FarmAnalytics } from '../../lib/analytics/types';
|
||||
|
||||
export default function FarmAnalyticsPage() {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<FarmAnalytics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/analytics/farms?timeRange=${timeRange}`);
|
||||
const result = await response.json();
|
||||
setData(result.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch farm analytics:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const zoneColumns = [
|
||||
{ key: 'zoneName', header: 'Zone' },
|
||||
{ key: 'currentCrop', header: 'Crop' },
|
||||
{
|
||||
key: 'healthScore',
|
||||
header: 'Health',
|
||||
align: 'right' as const,
|
||||
render: (v: number) => (
|
||||
<span className={`font-medium ${v >= 90 ? 'text-green-600' : v >= 70 ? 'text-yellow-600' : 'text-red-600'}`}>
|
||||
{v}%
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ key: 'yieldKg', header: 'Yield (kg)', align: 'right' as const, render: (v: number) => v.toFixed(1) },
|
||||
{ key: 'efficiency', header: 'Efficiency', align: 'right' as const, render: (v: number) => `${v}%` },
|
||||
];
|
||||
|
||||
const cropColumns = [
|
||||
{ key: 'cropType', header: 'Crop' },
|
||||
{ key: 'batches', header: 'Batches', align: 'right' as const },
|
||||
{ key: 'averageYieldKg', header: 'Avg Yield (kg)', align: 'right' as const, render: (v: number) => v.toFixed(1) },
|
||||
{ key: 'growthDays', header: 'Growth Days', align: 'right' as const },
|
||||
{
|
||||
key: 'successRate',
|
||||
header: 'Success Rate',
|
||||
align: 'right' as const,
|
||||
render: (v: number) => (
|
||||
<span className={`font-medium ${v >= 90 ? 'text-green-600' : v >= 75 ? 'text-yellow-600' : 'text-red-600'}`}>
|
||||
{v.toFixed(1)}%
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const predictionColumns = [
|
||||
{ key: 'cropType', header: 'Crop' },
|
||||
{ key: 'predictedYieldKg', header: 'Predicted Yield', align: 'right' as const, render: (v: number) => `${v.toFixed(1)} kg` },
|
||||
{ key: 'confidence', header: 'Confidence', align: 'right' as const, render: (v: number) => `${(v * 100).toFixed(0)}%` },
|
||||
{ key: 'harvestDate', header: 'Harvest Date' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-emerald-600 to-green-600 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold">Farm Analytics</h1>
|
||||
<p className="text-emerald-200 mt-1">Vertical farm performance and resource optimization</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Time Range Selector */}
|
||||
<div className="mb-8">
|
||||
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<Link href="/analytics">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Overview</a>
|
||||
</Link>
|
||||
<Link href="/analytics/plants">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Plants</a>
|
||||
</Link>
|
||||
<Link href="/analytics/transport">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Transport</a>
|
||||
</Link>
|
||||
<Link href="/analytics/farms">
|
||||
<a className="px-4 py-2 bg-emerald-500 text-white rounded-lg font-medium">Farms</a>
|
||||
</Link>
|
||||
<Link href="/analytics/sustainability">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Sustainability</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Total Farms"
|
||||
value={data?.totalFarms || 0}
|
||||
trend="up"
|
||||
color="green"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Active Zones"
|
||||
value={data?.totalZones || 0}
|
||||
trend="stable"
|
||||
color="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Active Batches"
|
||||
value={data?.activeBatches || 0}
|
||||
trend="up"
|
||||
color="purple"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Avg Yield"
|
||||
value={data?.averageYieldKg?.toFixed(1) || '0'}
|
||||
unit="kg/batch"
|
||||
trend="up"
|
||||
color="teal"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Resource Gauges */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
||||
<Gauge
|
||||
value={data?.resourceUsage?.waterEfficiency || 0}
|
||||
title="Water Efficiency"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={data?.resourceUsage?.energyEfficiency || 0}
|
||||
title="Energy Efficiency"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={(data?.completedBatches || 0) / ((data?.activeBatches || 1) + (data?.completedBatches || 0)) * 100}
|
||||
title="Completion Rate"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={90}
|
||||
title="Overall Health"
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Resource Usage Stats */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Resource Usage Summary</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-blue-600">
|
||||
{data?.resourceUsage?.waterLiters?.toLocaleString() || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Water Used (L)</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-yellow-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-yellow-600">
|
||||
{data?.resourceUsage?.energyKwh?.toLocaleString() || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Energy Used (kWh)</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-green-600">
|
||||
{data?.resourceUsage?.nutrientsKg?.toLocaleString() || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Nutrients Used (kg)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{data?.batchCompletionTrend && (
|
||||
<LineChart
|
||||
data={data.batchCompletionTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Batch Completions Over Time"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.topPerformingCrops && (
|
||||
<BarChart
|
||||
data={data.topPerformingCrops}
|
||||
xKey="cropType"
|
||||
yKey="averageYieldKg"
|
||||
title="Average Yield by Crop"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tables */}
|
||||
<div className="space-y-6">
|
||||
{data?.performanceByZone && (
|
||||
<DataTable
|
||||
data={data.performanceByZone}
|
||||
columns={zoneColumns}
|
||||
title="Zone Performance"
|
||||
pageSize={6}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.topPerformingCrops && (
|
||||
<DataTable
|
||||
data={data.topPerformingCrops}
|
||||
columns={cropColumns}
|
||||
title="Crop Performance"
|
||||
pageSize={6}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.yieldPredictions && (
|
||||
<DataTable
|
||||
data={data.yieldPredictions}
|
||||
columns={predictionColumns}
|
||||
title="Upcoming Harvest Predictions"
|
||||
pageSize={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
225
pages/analytics/index.tsx
Normal file
225
pages/analytics/index.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* Analytics Dashboard - Main Page
|
||||
* Comprehensive overview of all analytics data
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
KPICard,
|
||||
DateRangePicker,
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
} from '../../components/analytics';
|
||||
import { TimeRange, AnalyticsOverview, PlantAnalytics, TransportAnalytics } from '../../lib/analytics/types';
|
||||
|
||||
export default function AnalyticsDashboard() {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [overview, setOverview] = useState<AnalyticsOverview | null>(null);
|
||||
const [plantData, setPlantData] = useState<PlantAnalytics | null>(null);
|
||||
const [transportData, setTransportData] = useState<TransportAnalytics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [overviewRes, plantRes, transportRes] = await Promise.all([
|
||||
fetch(`/api/analytics/overview?timeRange=${timeRange}`),
|
||||
fetch(`/api/analytics/plants?timeRange=${timeRange}`),
|
||||
fetch(`/api/analytics/transport?timeRange=${timeRange}`),
|
||||
]);
|
||||
|
||||
const overviewData = await overviewRes.json();
|
||||
const plantDataRes = await plantRes.json();
|
||||
const transportDataRes = await transportRes.json();
|
||||
|
||||
setOverview(overviewData.data);
|
||||
setPlantData(plantDataRes.data);
|
||||
setTransportData(transportDataRes.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch analytics data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = (format: 'csv' | 'json') => {
|
||||
window.open(`/api/analytics/export?format=${format}&timeRange=${timeRange}`, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-green-600 to-emerald-600 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Analytics Dashboard</h1>
|
||||
<p className="text-green-200 mt-1">
|
||||
Comprehensive insights into your LocalGreenChain network
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => handleExport('csv')}
|
||||
className="px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport('json')}
|
||||
className="px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Export JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Time Range Selector */}
|
||||
<div className="mb-8">
|
||||
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<Link href="/analytics">
|
||||
<a className="px-4 py-2 bg-green-500 text-white rounded-lg font-medium">Overview</a>
|
||||
</Link>
|
||||
<Link href="/analytics/plants">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Plants</a>
|
||||
</Link>
|
||||
<Link href="/analytics/transport">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Transport</a>
|
||||
</Link>
|
||||
<Link href="/analytics/farms">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Farms</a>
|
||||
</Link>
|
||||
<Link href="/analytics/sustainability">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Sustainability</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Total Plants"
|
||||
value={overview?.totalPlants || 0}
|
||||
trend={overview?.trendsData?.[0]?.direction || 'stable'}
|
||||
changePercent={overview?.trendsData?.[0]?.changePercent}
|
||||
color="green"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Carbon Saved"
|
||||
value={transportData?.carbonSavedKg?.toFixed(1) || '0'}
|
||||
unit="kg CO2"
|
||||
trend="up"
|
||||
changePercent={12.5}
|
||||
color="teal"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Food Miles"
|
||||
value={transportData?.totalDistanceKm?.toFixed(0) || '0'}
|
||||
unit="km"
|
||||
trend="down"
|
||||
changePercent={-8.7}
|
||||
color="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Active Users"
|
||||
value={overview?.activeUsers || 0}
|
||||
trend="up"
|
||||
changePercent={8.3}
|
||||
color="purple"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Plant Registrations Trend */}
|
||||
{plantData?.registrationsTrend && (
|
||||
<LineChart
|
||||
data={plantData.registrationsTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Plant Registrations Over Time"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Species Distribution */}
|
||||
{plantData?.plantsBySpecies && (
|
||||
<PieChart
|
||||
data={plantData.plantsBySpecies}
|
||||
dataKey="count"
|
||||
nameKey="species"
|
||||
title="Plants by Species"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Transport Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Transport Methods */}
|
||||
{transportData?.eventsByMethod && (
|
||||
<BarChart
|
||||
data={transportData.eventsByMethod}
|
||||
xKey="method"
|
||||
yKey="count"
|
||||
title="Transport Events by Method"
|
||||
height={300}
|
||||
horizontal
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Carbon Trend */}
|
||||
{transportData?.carbonTrend && (
|
||||
<LineChart
|
||||
data={transportData.carbonTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Carbon Emissions Trend"
|
||||
colors={['#ef4444']}
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Network Summary</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-gray-900">{overview?.plantsRegisteredToday || 0}</p>
|
||||
<p className="text-sm text-gray-500">Registered Today</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-gray-900">{overview?.plantsRegisteredThisWeek || 0}</p>
|
||||
<p className="text-sm text-gray-500">This Week</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-gray-900">{overview?.plantsRegisteredThisMonth || 0}</p>
|
||||
<p className="text-sm text-gray-500">This Month</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-green-600">{overview?.growthRate?.toFixed(1) || 0}%</p>
|
||||
<p className="text-sm text-gray-500">Growth Rate</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
pages/analytics/plants.tsx
Normal file
182
pages/analytics/plants.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Plant Analytics Page
|
||||
* Detailed analytics for plant registrations and lineage
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
KPICard,
|
||||
DateRangePicker,
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
DataTable,
|
||||
TrendIndicator,
|
||||
} from '../../components/analytics';
|
||||
import { TimeRange, PlantAnalytics } from '../../lib/analytics/types';
|
||||
|
||||
export default function PlantAnalyticsPage() {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<PlantAnalytics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/analytics/plants?timeRange=${timeRange}`);
|
||||
const result = await response.json();
|
||||
setData(result.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch plant analytics:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const speciesColumns = [
|
||||
{ key: 'species', header: 'Species' },
|
||||
{ key: 'count', header: 'Count', align: 'right' as const },
|
||||
{ key: 'percentage', header: '% Share', align: 'right' as const, render: (v: number) => `${v.toFixed(1)}%` },
|
||||
{ key: 'trend', header: 'Trend', render: (v: string) => <TrendIndicator direction={v as any} /> },
|
||||
];
|
||||
|
||||
const growerColumns = [
|
||||
{ key: 'name', header: 'Grower' },
|
||||
{ key: 'totalPlants', header: 'Plants', align: 'right' as const },
|
||||
{ key: 'totalSpecies', header: 'Species', align: 'right' as const },
|
||||
{ key: 'averageGeneration', header: 'Avg Gen', align: 'right' as const, render: (v: number) => v.toFixed(1) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-green-600 to-emerald-600 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold">Plant Analytics</h1>
|
||||
<p className="text-green-200 mt-1">Detailed insights into plant registrations and lineage</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Time Range Selector */}
|
||||
<div className="mb-8">
|
||||
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<Link href="/analytics">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Overview</a>
|
||||
</Link>
|
||||
<Link href="/analytics/plants">
|
||||
<a className="px-4 py-2 bg-green-500 text-white rounded-lg font-medium">Plants</a>
|
||||
</Link>
|
||||
<Link href="/analytics/transport">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Transport</a>
|
||||
</Link>
|
||||
<Link href="/analytics/farms">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Farms</a>
|
||||
</Link>
|
||||
<Link href="/analytics/sustainability">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Sustainability</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Total Plants"
|
||||
value={data?.totalPlants || 0}
|
||||
trend="up"
|
||||
color="green"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Unique Species"
|
||||
value={data?.plantsBySpecies?.length || 0}
|
||||
trend="stable"
|
||||
color="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Avg Lineage Depth"
|
||||
value={data?.averageLineageDepth?.toFixed(1) || '0'}
|
||||
unit="generations"
|
||||
trend="up"
|
||||
color="purple"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Top Growers"
|
||||
value={data?.topGrowers?.length || 0}
|
||||
trend="stable"
|
||||
color="teal"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{data?.registrationsTrend && (
|
||||
<LineChart
|
||||
data={data.registrationsTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Plant Registrations Over Time"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.plantsBySpecies && (
|
||||
<PieChart
|
||||
data={data.plantsBySpecies}
|
||||
dataKey="count"
|
||||
nameKey="species"
|
||||
title="Distribution by Species"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generation Distribution */}
|
||||
{data?.plantsByGeneration && (
|
||||
<div className="mb-8">
|
||||
<BarChart
|
||||
data={data.plantsByGeneration}
|
||||
xKey="generation"
|
||||
yKey="count"
|
||||
title="Plants by Generation"
|
||||
height={250}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tables */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{data?.plantsBySpecies && (
|
||||
<DataTable
|
||||
data={data.plantsBySpecies}
|
||||
columns={speciesColumns}
|
||||
title="Species Breakdown"
|
||||
pageSize={6}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.topGrowers && (
|
||||
<DataTable
|
||||
data={data.topGrowers}
|
||||
columns={growerColumns}
|
||||
title="Top Growers"
|
||||
pageSize={6}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
315
pages/analytics/sustainability.tsx
Normal file
315
pages/analytics/sustainability.tsx
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* Sustainability Analytics Page
|
||||
* Environmental impact and sustainability metrics
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
KPICard,
|
||||
DateRangePicker,
|
||||
LineChart,
|
||||
AreaChart,
|
||||
Gauge,
|
||||
DataTable,
|
||||
TrendIndicator,
|
||||
} from '../../components/analytics';
|
||||
import { TimeRange, SustainabilityAnalytics } from '../../lib/analytics/types';
|
||||
|
||||
export default function SustainabilityAnalyticsPage() {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<SustainabilityAnalytics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/analytics/sustainability?timeRange=${timeRange}`);
|
||||
const result = await response.json();
|
||||
setData(result.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sustainability analytics:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const goalColumns = [
|
||||
{ key: 'name', header: 'Goal' },
|
||||
{ key: 'current', header: 'Current', align: 'right' as const, render: (v: number, row: any) => `${v} ${row.unit}` },
|
||||
{ key: 'target', header: 'Target', align: 'right' as const, render: (v: number, row: any) => `${v} ${row.unit}` },
|
||||
{
|
||||
key: 'progress',
|
||||
header: 'Progress',
|
||||
align: 'right' as const,
|
||||
render: (v: number) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-20 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${v >= 90 ? 'bg-green-500' : v >= 70 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${Math.min(v, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm">{v}%</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: (v: string) => (
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded-full ${
|
||||
v === 'on_track'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: v === 'at_risk'
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
}`}
|
||||
>
|
||||
{v.replace('_', ' ')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-green-700 to-emerald-700 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold">Sustainability Analytics</h1>
|
||||
<p className="text-green-200 mt-1">Environmental impact and sustainability metrics</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Time Range Selector */}
|
||||
<div className="mb-8">
|
||||
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<Link href="/analytics">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Overview</a>
|
||||
</Link>
|
||||
<Link href="/analytics/plants">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Plants</a>
|
||||
</Link>
|
||||
<Link href="/analytics/transport">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Transport</a>
|
||||
</Link>
|
||||
<Link href="/analytics/farms">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Farms</a>
|
||||
</Link>
|
||||
<Link href="/analytics/sustainability">
|
||||
<a className="px-4 py-2 bg-green-700 text-white rounded-lg font-medium">Sustainability</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Overall Score */}
|
||||
<div className="bg-gradient-to-r from-green-500 to-emerald-500 rounded-lg p-8 mb-8 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Overall Sustainability Score</h2>
|
||||
<p className="text-green-100 mt-1">Based on carbon, local production, water, and waste metrics</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-6xl font-bold">{data?.overallScore?.toFixed(0) || 0}</div>
|
||||
<div className="text-green-100">out of 100</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Carbon Saved"
|
||||
value={data?.carbonFootprint?.totalSavedKg?.toFixed(0) || '0'}
|
||||
unit="kg CO2"
|
||||
trend="up"
|
||||
color="green"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Reduction Rate"
|
||||
value={data?.carbonFootprint?.reductionPercentage?.toFixed(0) || '0'}
|
||||
unit="%"
|
||||
trend="up"
|
||||
color="teal"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Local Production"
|
||||
value={data?.localProduction?.percentage?.toFixed(0) || '0'}
|
||||
unit="%"
|
||||
trend="up"
|
||||
color="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Water Efficiency"
|
||||
value={data?.waterUsage?.efficiencyScore?.toFixed(0) || '0'}
|
||||
unit="%"
|
||||
trend="up"
|
||||
color="purple"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gauges */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
||||
<Gauge
|
||||
value={data?.carbonFootprint?.reductionPercentage || 0}
|
||||
title="Carbon Reduction"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={data?.foodMiles?.localPercentage || 0}
|
||||
title="Local Sourcing"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={data?.waterUsage?.efficiencyScore || 0}
|
||||
title="Water Efficiency"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={data?.overallScore || 0}
|
||||
title="Sustainability Score"
|
||||
unit="pts"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Impact Metrics */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Environmental Impact</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-green-600">
|
||||
{data?.carbonFootprint?.equivalentTrees?.toFixed(1) || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Trees Equivalent</p>
|
||||
<p className="text-xs text-gray-400 mt-1">CO2 absorption per year</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-blue-600">
|
||||
{data?.foodMiles?.savedMiles?.toLocaleString() || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Miles Saved</p>
|
||||
<p className="text-xs text-gray-400 mt-1">vs conventional transport</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-cyan-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-cyan-600">
|
||||
{data?.waterUsage?.savedLiters?.toLocaleString() || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Liters Saved</p>
|
||||
<p className="text-xs text-gray-400 mt-1">Water conservation</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
||||
<p className="text-3xl font-bold text-purple-600">
|
||||
{data?.localProduction?.localCount || 0}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Local Plants</p>
|
||||
<p className="text-xs text-gray-400 mt-1">Within 50km radius</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{data?.carbonFootprint?.monthlyTrend && (
|
||||
<AreaChart
|
||||
data={data.carbonFootprint.monthlyTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Carbon Emissions Trend"
|
||||
colors={['#10b981']}
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.foodMiles?.monthlyTrend && (
|
||||
<AreaChart
|
||||
data={data.foodMiles.monthlyTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Food Miles Trend"
|
||||
colors={['#3b82f6']}
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sustainability Trends */}
|
||||
{data?.trends && data.trends.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<LineChart
|
||||
data={data.trends[0].values}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Sustainability Trend Over Time"
|
||||
colors={['#10b981', '#3b82f6']}
|
||||
height={250}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Goals Table */}
|
||||
{data?.goals && (
|
||||
<DataTable
|
||||
data={data.goals}
|
||||
columns={goalColumns}
|
||||
title="Sustainability Goals Progress"
|
||||
pageSize={10}
|
||||
showSearch={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tips Section */}
|
||||
<div className="mt-8 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-200 p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Sustainability Recommendations</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-green-500 mt-1">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-gray-600">Increase local sourcing to reduce transport emissions</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-green-500 mt-1">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-gray-600">Use electric or bicycle delivery for short distances</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-green-500 mt-1">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-gray-600">Optimize water usage in vertical farming operations</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-green-500 mt-1">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<p className="text-sm text-gray-600">Consolidate deliveries to reduce carbon footprint</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
232
pages/analytics/transport.tsx
Normal file
232
pages/analytics/transport.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* Transport Analytics Page
|
||||
* Carbon footprint and food miles analysis
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
KPICard,
|
||||
DateRangePicker,
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
AreaChart,
|
||||
Gauge,
|
||||
DataTable,
|
||||
} from '../../components/analytics';
|
||||
import { TimeRange, TransportAnalytics } from '../../lib/analytics/types';
|
||||
|
||||
export default function TransportAnalyticsPage() {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<TransportAnalytics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [timeRange]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/analytics/transport?timeRange=${timeRange}`);
|
||||
const result = await response.json();
|
||||
setData(result.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch transport analytics:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const methodColumns = [
|
||||
{ key: 'method', header: 'Method' },
|
||||
{ key: 'count', header: 'Events', align: 'right' as const },
|
||||
{ key: 'distanceKm', header: 'Distance (km)', align: 'right' as const, render: (v: number) => v.toLocaleString() },
|
||||
{ key: 'carbonKg', header: 'Carbon (kg)', align: 'right' as const, render: (v: number) => v.toFixed(2) },
|
||||
{
|
||||
key: 'efficiency',
|
||||
header: 'Efficiency',
|
||||
align: 'right' as const,
|
||||
render: (v: number) => (
|
||||
<span className={`font-medium ${v >= 80 ? 'text-green-600' : v >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
|
||||
{v}%
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const routeColumns = [
|
||||
{ key: 'from', header: 'From' },
|
||||
{ key: 'to', header: 'To' },
|
||||
{ key: 'method', header: 'Method' },
|
||||
{ key: 'distanceKm', header: 'Distance', align: 'right' as const, render: (v: number) => `${v} km` },
|
||||
{ key: 'frequency', header: 'Frequency', align: 'right' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-teal-600 to-cyan-600 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold">Transport Analytics</h1>
|
||||
<p className="text-teal-200 mt-1">Carbon footprint and food miles analysis</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Time Range Selector */}
|
||||
<div className="mb-8">
|
||||
<DateRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex space-x-4 mb-8">
|
||||
<Link href="/analytics">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Overview</a>
|
||||
</Link>
|
||||
<Link href="/analytics/plants">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Plants</a>
|
||||
</Link>
|
||||
<Link href="/analytics/transport">
|
||||
<a className="px-4 py-2 bg-teal-500 text-white rounded-lg font-medium">Transport</a>
|
||||
</Link>
|
||||
<Link href="/analytics/farms">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Farms</a>
|
||||
</Link>
|
||||
<Link href="/analytics/sustainability">
|
||||
<a className="px-4 py-2 bg-white hover:bg-gray-50 text-gray-700 rounded-lg font-medium shadow">Sustainability</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<KPICard
|
||||
title="Total Events"
|
||||
value={data?.totalEvents || 0}
|
||||
trend="up"
|
||||
color="blue"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Total Distance"
|
||||
value={data?.totalDistanceKm?.toLocaleString() || '0'}
|
||||
unit="km"
|
||||
trend="stable"
|
||||
color="purple"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Carbon Emitted"
|
||||
value={data?.totalCarbonKg?.toFixed(1) || '0'}
|
||||
unit="kg CO2"
|
||||
trend="down"
|
||||
color="orange"
|
||||
loading={loading}
|
||||
/>
|
||||
<KPICard
|
||||
title="Carbon Saved"
|
||||
value={data?.carbonSavedKg?.toFixed(1) || '0'}
|
||||
unit="kg CO2"
|
||||
trend="up"
|
||||
color="green"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gauges */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
||||
<Gauge
|
||||
value={data?.totalCarbonKg ? (data.carbonSavedKg / (data.totalCarbonKg + data.carbonSavedKg)) * 100 : 0}
|
||||
title="Carbon Reduction"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={100 - ((data?.averageDistancePerEvent || 0) / 50) * 100}
|
||||
title="Distance Efficiency"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={data?.eventsByMethod?.filter(m => m.efficiency >= 80).length
|
||||
? (data.eventsByMethod.filter(m => m.efficiency >= 80).reduce((s, m) => s + m.count, 0) / data.totalEvents) * 100
|
||||
: 0}
|
||||
title="Green Transport"
|
||||
unit="%"
|
||||
/>
|
||||
<Gauge
|
||||
value={75}
|
||||
title="Local Sourcing"
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{data?.carbonTrend && (
|
||||
<AreaChart
|
||||
data={data.carbonTrend}
|
||||
xKey="label"
|
||||
yKey="value"
|
||||
title="Carbon Emissions Trend"
|
||||
colors={['#f59e0b']}
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.eventsByMethod && (
|
||||
<BarChart
|
||||
data={data.eventsByMethod}
|
||||
xKey="method"
|
||||
yKey="carbonKg"
|
||||
title="Carbon by Transport Method"
|
||||
height={300}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event Types Pie Chart */}
|
||||
{data?.eventsByType && (
|
||||
<div className="mb-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<PieChart
|
||||
data={data.eventsByType}
|
||||
dataKey="count"
|
||||
nameKey="eventType"
|
||||
title="Events by Type"
|
||||
height={300}
|
||||
/>
|
||||
<PieChart
|
||||
data={data.eventsByMethod}
|
||||
dataKey="distanceKm"
|
||||
nameKey="method"
|
||||
title="Distance by Method"
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tables */}
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{data?.eventsByMethod && (
|
||||
<DataTable
|
||||
data={data.eventsByMethod}
|
||||
columns={methodColumns}
|
||||
title="Transport Method Breakdown"
|
||||
pageSize={8}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.mostEfficientRoutes && (
|
||||
<DataTable
|
||||
data={data.mostEfficientRoutes}
|
||||
columns={routeColumns}
|
||||
title="Most Efficient Routes"
|
||||
pageSize={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
155
pages/api/analytics/export.ts
Normal file
155
pages/api/analytics/export.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Analytics Export API
|
||||
* Exports analytics data in various formats (CSV, JSON)
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
getAnalyticsOverview,
|
||||
getPlantAnalytics,
|
||||
getTransportAnalytics,
|
||||
getFarmAnalytics,
|
||||
getSustainabilityAnalytics,
|
||||
TimeRange,
|
||||
AnalyticsFilters,
|
||||
} from '../../../lib/analytics';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ExportData {
|
||||
overview?: any;
|
||||
plants?: any;
|
||||
transport?: any;
|
||||
farms?: any;
|
||||
sustainability?: any;
|
||||
}
|
||||
|
||||
function convertToCSV(data: any[], headers: string[]): string {
|
||||
const headerRow = headers.join(',');
|
||||
const rows = data.map(item =>
|
||||
headers.map(header => {
|
||||
const value = item[header];
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string' && value.includes(',')) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return String(value);
|
||||
}).join(',')
|
||||
);
|
||||
return [headerRow, ...rows].join('\n');
|
||||
}
|
||||
|
||||
function generatePlantCSV(plantData: any): string {
|
||||
const speciesData = plantData.plantsBySpecies.map((s: any) => ({
|
||||
species: s.species,
|
||||
count: s.count,
|
||||
percentage: s.percentage,
|
||||
trend: s.trend,
|
||||
}));
|
||||
return convertToCSV(speciesData, ['species', 'count', 'percentage', 'trend']);
|
||||
}
|
||||
|
||||
function generateTransportCSV(transportData: any): string {
|
||||
const methodData = transportData.eventsByMethod.map((m: any) => ({
|
||||
method: m.method,
|
||||
count: m.count,
|
||||
distanceKm: m.distanceKm,
|
||||
carbonKg: m.carbonKg,
|
||||
efficiency: m.efficiency,
|
||||
}));
|
||||
return convertToCSV(methodData, ['method', 'count', 'distanceKm', 'carbonKg', 'efficiency']);
|
||||
}
|
||||
|
||||
function generateSustainabilityCSV(sustainData: any): string {
|
||||
const goalsData = sustainData.goals.map((g: any) => ({
|
||||
name: g.name,
|
||||
target: g.target,
|
||||
current: g.current,
|
||||
unit: g.unit,
|
||||
progress: g.progress,
|
||||
status: g.status,
|
||||
}));
|
||||
return convertToCSV(goalsData, ['name', 'target', 'current', 'unit', 'progress', 'status']);
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const exportFormat = (req.query.format as string) || 'json';
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
const sections = req.query.sections
|
||||
? (req.query.sections as string).split(',')
|
||||
: ['overview', 'plants', 'transport', 'farms', 'sustainability'];
|
||||
|
||||
const filters: AnalyticsFilters = { timeRange };
|
||||
const exportData: ExportData = {};
|
||||
|
||||
// Fetch requested sections
|
||||
if (sections.includes('overview')) {
|
||||
exportData.overview = await getAnalyticsOverview(filters);
|
||||
}
|
||||
if (sections.includes('plants')) {
|
||||
exportData.plants = await getPlantAnalytics(filters);
|
||||
}
|
||||
if (sections.includes('transport')) {
|
||||
exportData.transport = await getTransportAnalytics(filters);
|
||||
}
|
||||
if (sections.includes('farms')) {
|
||||
exportData.farms = await getFarmAnalytics(filters);
|
||||
}
|
||||
if (sections.includes('sustainability')) {
|
||||
exportData.sustainability = await getSustainabilityAnalytics(filters);
|
||||
}
|
||||
|
||||
const timestamp = format(new Date(), 'yyyy-MM-dd_HH-mm-ss');
|
||||
|
||||
if (exportFormat === 'csv') {
|
||||
// Generate combined CSV
|
||||
let csvContent = '';
|
||||
|
||||
if (exportData.plants) {
|
||||
csvContent += '# Plant Analytics - Species Distribution\n';
|
||||
csvContent += generatePlantCSV(exportData.plants);
|
||||
csvContent += '\n\n';
|
||||
}
|
||||
|
||||
if (exportData.transport) {
|
||||
csvContent += '# Transport Analytics - By Method\n';
|
||||
csvContent += generateTransportCSV(exportData.transport);
|
||||
csvContent += '\n\n';
|
||||
}
|
||||
|
||||
if (exportData.sustainability) {
|
||||
csvContent += '# Sustainability Goals\n';
|
||||
csvContent += generateSustainabilityCSV(exportData.sustainability);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=analytics_export_${timestamp}.csv`);
|
||||
return res.status(200).send(csvContent);
|
||||
}
|
||||
|
||||
// Default: JSON format
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=analytics_export_${timestamp}.json`);
|
||||
|
||||
return res.status(200).json({
|
||||
exportedAt: new Date().toISOString(),
|
||||
timeRange,
|
||||
sections,
|
||||
data: exportData,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Analytics export error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to export analytics data',
|
||||
});
|
||||
}
|
||||
}
|
||||
46
pages/api/analytics/farms.ts
Normal file
46
pages/api/analytics/farms.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Farm Analytics API
|
||||
* Returns vertical farm analytics data
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getFarmAnalytics, AnalyticsFilters, TimeRange } from '../../../lib/analytics';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
const farmIds = req.query.farmIds
|
||||
? (req.query.farmIds as string).split(',')
|
||||
: undefined;
|
||||
|
||||
const filters: AnalyticsFilters = {
|
||||
timeRange,
|
||||
farmIds,
|
||||
};
|
||||
|
||||
const farmAnalytics = await getFarmAnalytics(filters);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: farmAnalytics,
|
||||
meta: {
|
||||
timeRange,
|
||||
filters: { farmIds },
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Farm analytics error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch farm analytics',
|
||||
});
|
||||
}
|
||||
}
|
||||
41
pages/api/analytics/overview.ts
Normal file
41
pages/api/analytics/overview.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Analytics Overview API
|
||||
* Returns aggregated overview metrics for the dashboard
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getAnalyticsOverview, AnalyticsFilters, TimeRange } from '../../../lib/analytics';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
|
||||
const filters: AnalyticsFilters = {
|
||||
timeRange,
|
||||
};
|
||||
|
||||
const overview = await getAnalyticsOverview(filters);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: overview,
|
||||
meta: {
|
||||
timeRange,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Analytics overview error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch analytics overview',
|
||||
});
|
||||
}
|
||||
}
|
||||
44
pages/api/analytics/plants.ts
Normal file
44
pages/api/analytics/plants.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Plant Analytics API
|
||||
* Returns plant-specific analytics data
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getPlantAnalytics, AnalyticsFilters, TimeRange } from '../../../lib/analytics';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
const species = req.query.species ? (req.query.species as string).split(',') : undefined;
|
||||
|
||||
const filters: AnalyticsFilters = {
|
||||
timeRange,
|
||||
species,
|
||||
};
|
||||
|
||||
const plantAnalytics = await getPlantAnalytics(filters);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: plantAnalytics,
|
||||
meta: {
|
||||
timeRange,
|
||||
filters: { species },
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Plant analytics error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch plant analytics',
|
||||
});
|
||||
}
|
||||
}
|
||||
41
pages/api/analytics/sustainability.ts
Normal file
41
pages/api/analytics/sustainability.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Sustainability Analytics API
|
||||
* Returns environmental impact and sustainability metrics
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getSustainabilityAnalytics, AnalyticsFilters, TimeRange } from '../../../lib/analytics';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
|
||||
const filters: AnalyticsFilters = {
|
||||
timeRange,
|
||||
};
|
||||
|
||||
const sustainabilityAnalytics = await getSustainabilityAnalytics(filters);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: sustainabilityAnalytics,
|
||||
meta: {
|
||||
timeRange,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Sustainability analytics error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch sustainability analytics',
|
||||
});
|
||||
}
|
||||
}
|
||||
46
pages/api/analytics/transport.ts
Normal file
46
pages/api/analytics/transport.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Transport Analytics API
|
||||
* Returns transport and carbon footprint analytics
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getTransportAnalytics, AnalyticsFilters, TimeRange } from '../../../lib/analytics';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const timeRange = (req.query.timeRange as TimeRange) || '30d';
|
||||
const transportMethods = req.query.methods
|
||||
? (req.query.methods as string).split(',')
|
||||
: undefined;
|
||||
|
||||
const filters: AnalyticsFilters = {
|
||||
timeRange,
|
||||
transportMethods,
|
||||
};
|
||||
|
||||
const transportAnalytics = await getTransportAnalytics(filters);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: transportAnalytics,
|
||||
meta: {
|
||||
timeRange,
|
||||
filters: { transportMethods },
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Transport analytics error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch transport analytics',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"downlevelIteration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue