From feb3cef7118afc6bf81fc6c4daf5c40d646b47e4 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Sat, 28 Feb 2026 04:11:14 +0100 Subject: [PATCH] feat: Admin Escrow Analytics Dashboard --- package-lock.json | 118 +++--- src/app/admin/page.tsx | 110 ++++++ .../dashboard/dashboard-01/Dashboard.tsx | 315 ++++++++++++++++ .../dashboard/dashboard-01/chart.tsx | 84 +++++ .../dashboard/dashboard-01/useDashboard.ts | 119 ++++++ .../AdminEscrowAnalyticsDashboard.tsx | 339 ++++++++++++++++++ .../escrows/admin-analytics/aggregation.ts | 132 +++++++ .../escrows/admin-analytics/index.ts | 3 + .../useAdminEscrowAnalytics.ts | 98 +++++ 9 files changed, 1273 insertions(+), 45 deletions(-) create mode 100644 src/app/admin/page.tsx create mode 100644 src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx create mode 100644 src/components/tw-blocks/dashboard/dashboard-01/chart.tsx create mode 100644 src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts create mode 100644 src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx create mode 100644 src/components/tw-blocks/escrows/admin-analytics/aggregation.ts create mode 100644 src/components/tw-blocks/escrows/admin-analytics/index.ts create mode 100644 src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts diff --git a/package-lock.json b/package-lock.json index 7024719..5988a3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -367,6 +366,7 @@ "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@stellar/stellar-base": "^13.1.0", "axios": "^1.8.4", @@ -386,6 +386,7 @@ "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", @@ -446,7 +447,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.1" }, @@ -529,6 +529,21 @@ } } }, + "node_modules/@creit-tech/stellar-wallets-kit/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@creit.tech/xbull-wallet-connect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", @@ -1973,6 +1988,7 @@ "resolved": "https://registry.npmjs.org/@near-js/accounts/-/accounts-1.4.1.tgz", "integrity": "sha512-ni3QT9H3NdrbVVKyx56yvz93r89Dvpc/vgVtiIK2OdXjkK6jcj+UKMDRQ6F7rd9qJOInLkHZbVBtcR6j1CXLjw==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/providers": "1.0.3", @@ -1992,7 +2008,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@near-js/crypto": { "version": "1.4.2", @@ -2019,6 +2036,7 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores/-/keystores-0.2.2.tgz", "integrity": "sha512-DLhi/3a4qJUY+wgphw2Jl4S+L0AKsUYm1mtU0WxKYV5OBwjOXvbGrXNfdkheYkfh3nHwrQgtjvtszX6LrRXLLw==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/types": "0.3.1" @@ -2029,6 +2047,7 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores-browser/-/keystores-browser-0.2.2.tgz", "integrity": "sha512-Pxqm7WGtUu6zj32vGCy9JcEDpZDSB5CCaLQDTQdF3GQyL0flyRv2I/guLAgU5FLoYxU7dJAX9mslJhPW7P2Bfw==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2" @@ -2039,6 +2058,7 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores-node/-/keystores-node-0.1.2.tgz", "integrity": "sha512-MWLvTszZOVziiasqIT/LYNhUyWqOJjDGlsthOsY6dTL4ZcXjjmhmzrbFydIIeQr+CcEl5wukTo68ORI9JrHl6g==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2" @@ -2049,6 +2069,7 @@ "resolved": "https://registry.npmjs.org/@near-js/providers/-/providers-1.0.3.tgz", "integrity": "sha512-VJMboL14R/+MGKnlhhE3UPXCGYvMd1PpvF9OqZ9yBbulV7QVSIdTMfY4U1NnDfmUC2S3/rhAEr+3rMrIcNS7Fg==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/transactions": "1.3.3", "@near-js/types": "0.3.1", @@ -2064,7 +2085,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@near-js/providers/node_modules/node-fetch": { "version": "2.6.7", @@ -2072,6 +2094,7 @@ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2092,14 +2115,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@near-js/providers/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause", - "optional": true + "optional": true, + "peer": true }, "node_modules/@near-js/providers/node_modules/whatwg-url": { "version": "5.0.0", @@ -2107,6 +2132,7 @@ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -2117,6 +2143,7 @@ "resolved": "https://registry.npmjs.org/@near-js/signers/-/signers-0.2.2.tgz", "integrity": "sha512-M6ib+af9zXAPRCjH2RyIS0+RhCmd9gxzCeIkQ+I2A3zjgGiEDkBZbYso9aKj8Zh2lPKKSH7h+u8JGymMOSwgyw==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2", @@ -2128,6 +2155,7 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 16" }, @@ -2140,6 +2168,7 @@ "resolved": "https://registry.npmjs.org/@near-js/transactions/-/transactions-1.3.3.tgz", "integrity": "sha512-1AXD+HuxlxYQmRTLQlkVmH+RAmV3HwkAT8dyZDu+I2fK/Ec9BQHXakOJUnOBws3ihF+akQhamIBS5T0EXX/Ylw==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/signers": "0.2.2", @@ -2153,7 +2182,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@near-js/types": { "version": "0.3.1", @@ -2178,6 +2208,7 @@ "resolved": "https://registry.npmjs.org/@near-js/wallet-account/-/wallet-account-1.3.3.tgz", "integrity": "sha512-GDzg/Kz0GBYF7tQfyQQQZ3vviwV8yD+8F2lYDzsWJiqIln7R1ov0zaXN4Tii86TeS21KPn2hHAsVu3Y4txa8OQ==", "license": "ISC", + "peer": true, "dependencies": { "@near-js/accounts": "1.4.1", "@near-js/crypto": "1.4.2", @@ -2194,7 +2225,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@near-wallet-selector/core": { "version": "8.10.2", @@ -4931,7 +4963,6 @@ "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-2.3.0.tgz", "integrity": "sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==", "license": "MIT", - "peer": true, "dependencies": { "@solana/accounts": "2.3.0", "@solana/addresses": "2.3.0", @@ -5294,7 +5325,6 @@ "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-2.3.0.tgz", "integrity": "sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==", "license": "MIT", - "peer": true, "dependencies": { "@solana/accounts": "2.3.0", "@solana/codecs": "2.3.0", @@ -5423,7 +5453,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -5924,7 +5953,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.0.tgz", "integrity": "sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.75.0" }, @@ -6231,7 +6259,6 @@ "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.6.4.tgz", "integrity": "sha512-/N3hhOFIIhufvihCx92wvxd15Wy9XAJOSbTiV8rYG2N9uBvzejctNO2+LpwCRl/cBKle9rsp4S7C/zz++iDuOg==", "license": "SEE LICENSE IN LICENSE.md", - "peer": true, "dependencies": { "@ethereumjs/common": "^10.0.0", "@ethereumjs/tx": "^10.0.0", @@ -6676,7 +6703,6 @@ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6688,7 +6714,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -6864,7 +6889,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -8434,7 +8458,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9173,7 +9196,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -10354,7 +10376,6 @@ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -10418,7 +10439,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10604,7 +10624,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10732,7 +10751,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10944,7 +10962,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/eyes": { "version": "0.1.8", @@ -11225,6 +11244,7 @@ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", "license": "MIT", + "peer": true, "dependencies": { "is-property": "^1.0.2" } @@ -11234,6 +11254,7 @@ "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", "license": "MIT", + "peer": true, "dependencies": { "is-property": "^1.0.0" } @@ -11626,6 +11647,7 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "license": "MIT", + "peer": true, "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -11642,6 +11664,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -11650,7 +11673,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/humanize-ms": { "version": "1.2.1", @@ -11665,8 +11689,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/ieee754": { "version": "1.2.1", @@ -12021,13 +12044,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-my-json-valid": { "version": "2.20.6", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", "license": "MIT", + "peer": true, "dependencies": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", @@ -12080,7 +12105,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-regex": { "version": "1.2.1", @@ -12378,7 +12404,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -12475,6 +12500,7 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12942,7 +12968,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lru-cache": { "version": "5.1.1", @@ -13179,6 +13206,7 @@ "resolved": "https://registry.npmjs.org/near-abi/-/near-abi-0.2.0.tgz", "integrity": "sha512-kCwSf/3fraPU2zENK18sh+kKG4uKbEUEQdyWQkmW8ZofmLarObIz2+zAYjA1teDZLeMvEQew3UysnPDXgjneaA==", "license": "(MIT AND Apache-2.0)", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.11" } @@ -13188,6 +13216,7 @@ "resolved": "https://registry.npmjs.org/near-api-js/-/near-api-js-5.1.1.tgz", "integrity": "sha512-h23BGSKxNv8ph+zU6snicstsVK1/CTXsQz4LuGGwoRE24Hj424nSe4+/1tzoiC285Ljf60kPAqRCmsfv9etF2g==", "license": "(MIT AND Apache-2.0)", + "peer": true, "dependencies": { "@near-js/accounts": "1.4.1", "@near-js/crypto": "1.4.2", @@ -13212,13 +13241,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/near-api-js/node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13238,19 +13269,22 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/near-api-js/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/near-api-js/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -13898,7 +13932,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13955,7 +13988,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -14169,7 +14201,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -14203,7 +14234,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -14217,7 +14247,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14881,7 +14910,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/sha.js": { "version": "2.4.12", @@ -15189,6 +15219,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -15634,7 +15665,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15674,6 +15704,7 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.6" } @@ -15757,8 +15788,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsup": { "version": "8.3.5", @@ -15943,7 +15973,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16595,7 +16624,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -16644,6 +16672,7 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.4" } @@ -16766,7 +16795,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..0b03f74 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { AdminEscrowAnalyticsDashboard } from "@/components/tw-blocks/escrows/admin-analytics"; +import { WalletButton } from "@/components/tw-blocks/wallet-kit/WalletButtons"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Image from "next/image"; +import Link from "next/link"; +import { ArrowLeft, Search, Filter } from "lucide-react"; + +export default function AdminPage() { + const [inputValue, setInputValue] = useState(""); + const [engagementId, setEngagementId] = useState(""); + + useEffect(() => { + const saved = localStorage.getItem("admin_last_engagement_id"); + if (saved) { + setInputValue(saved); + setEngagementId(saved); + } + }, []); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setEngagementId(inputValue); + if (inputValue) { + localStorage.setItem("admin_last_engagement_id", inputValue); + } else { + localStorage.removeItem("admin_last_engagement_id"); + } + }; + + return ( +
+
+
+ + + +
+ Trustless Work +

+ Admin Portal +

+
+
+ + +
+ +
+
+
+

Admin Overview

+

+ Monitor escrow performance, financial breakdown, and engagement health metrics across the Trustless Work ecosystem. +

+
+ +
+
+
+ +
+
+ + setInputValue(e.target.value)} + className="h-11 pl-10 text-base border-border/60 focus:border-primary/60 bg-background/80 transition-shadow shadow-xs focus:shadow-md" + /> +
+ +
+
+
+
+
+ +
+ {engagementId ? ( + + ) : ( +
+
+ +
+
+

No Engagement Selected

+

Search for an engagement ID above to view the dashboard.

+
+
+ )} +
+
+
+ ); +} diff --git a/src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx b/src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx new file mode 100644 index 0000000..053001a --- /dev/null +++ b/src/components/tw-blocks/dashboard/dashboard-01/Dashboard.tsx @@ -0,0 +1,315 @@ +"use client"; + +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { useDashboard } from "./useDashboard"; +import { formatCurrency } from "../../helpers/format.helper"; +import { Activity, Layers3, PiggyBank, CloudOff } from "lucide-react"; +import { + AreaChart, + Area, + XAxis, + CartesianGrid, + BarChart, + Bar, + PieChart, + Pie, + Cell, +} from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; +import { + Empty, + EmptyHeader, + EmptyMedia, + EmptyTitle, + EmptyDescription, +} from "@/components/ui/empty"; + +const chartConfigBar: ChartConfig = { + desktop: { + label: "Amount", + color: "var(--chart-1)", + }, +}; + +const chartConfigDonut: ChartConfig = { + visitors: { label: "Count" }, + single: { label: "Single", color: "var(--chart-1)" }, + multi: { label: "Multi", color: "var(--chart-2)" }, +}; + +const chartConfigArea: ChartConfig = { + desktop: { + label: "Created", + color: "var(--chart-1)", + }, +}; + +export const Dashboard01 = () => { + const { + isLoading, + totalEscrows, + totalAmount, + totalBalance, + typeSlices, + amountsByDate, + createdByDate, + } = useDashboard(); + + const barData = React.useMemo( + () => amountsByDate.map((d) => ({ month: d.date, desktop: d.amount })), + [amountsByDate] + ); + + const donutData = React.useMemo( + () => + typeSlices.map((s) => ({ + browser: s.type === "single" ? "single" : "multi", + visitors: s.value, + fill: + s.type === "single" ? "var(--color-single)" : "var(--color-multi)", + })), + [typeSlices] + ); + + const areaData = React.useMemo( + () => createdByDate.map((d) => ({ month: d.date, desktop: d.count })), + [createdByDate] + ); + + return ( +
+ {/* KPI Cards */} +
+ + + Escrows + + + +
+ {isLoading ? "-" : totalEscrows} +
+

+ Total number of escrows +

+
+
+ + + + Total Amount + + + +
+ {isLoading ? "-" : formatCurrency(totalAmount, "USDC")} +
+

+ Sum of amounts (SR + MR) +

+
+
+ + + + Total Balance + + + +
+ {isLoading ? "-" : formatCurrency(totalBalance, "USDC")} +
+

+ Total balance across all escrows +

+
+
+
+ + + + {/* Charts */} +
+ {/* Bar chart: Amounts by date (shadcn pattern) */} + + + Escrow Amounts + Amounts by date + + + + {barData.length > 0 ? ( + + + + new Date(String(value)).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + } + /> + } + /> + + + ) : ( + + + + + + No data + No Data Available + + + )} + + + + + {/* Donut chart: Escrow types (shadcn pattern) */} + + + Escrow Types + Escrow types + + + + {donutData.some((d) => Number(d.visitors) > 0) ? ( + + } + /> + + {donutData.map((slice, idx) => ( + + ))} + + + ) : ( + + + + + + No data + No Data Available + + + )} + +
+
+ + Single +
+
+ + Multi +
+
+
+
+ + {/* Area chart: Created escrows (shadcn pattern) */} + + + Escrow Created + Created escrows by date + + + + {areaData.length > 0 ? ( + + + + new Date(String(value)).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + } + /> + } + /> + + + ) : ( + + + + + + No data + No Data Available + + + )} + + + +
+
+ ); +}; diff --git a/src/components/tw-blocks/dashboard/dashboard-01/chart.tsx b/src/components/tw-blocks/dashboard/dashboard-01/chart.tsx new file mode 100644 index 0000000..80d0bcb --- /dev/null +++ b/src/components/tw-blocks/dashboard/dashboard-01/chart.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { Tooltip as RechartsTooltip } from "recharts"; + +export type ChartConfig = Record; + +interface ChartContainerProps { + config: ChartConfig; + className?: string; + children: React.ReactNode; +} + +export function ChartContainer({ + config, + className, + children, +}: ChartContainerProps) { + const style: React.CSSProperties = {}; + for (const [key, value] of Object.entries(config)) { + const varName = `--color-${key}` as const; + if (value.color) (style as any)[varName] = value.color; + } + return ( +
+ {children} +
+ ); +} + +type RechartsPayloadItem = { + value?: number | string; + dataKey?: string; + color?: string; + name?: string; +}; + +type RechartsTooltipContentProps = { + active?: boolean; + label?: string | number; + payload?: RechartsPayloadItem[]; +}; + +export type ChartTooltipContentProps = { + hideLabel?: boolean; + indicator?: "line" | "dot"; +} & RechartsTooltipContentProps; + +export function ChartTooltip( + props: React.ComponentProps +) { + return ; +} + +export function ChartTooltipContent({ + active, + label, + payload, + hideLabel, +}: ChartTooltipContentProps) { + if (!active || !payload || payload.length === 0) return null; + return ( +
+ {!hideLabel ?
{label}
: null} +
+ {payload.map((item, idx) => ( +
+ + + {item.name ?? String(item.dataKey)} + + + {item.value as React.ReactNode} + +
+ ))} +
+
+ ); +} diff --git a/src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts b/src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts new file mode 100644 index 0000000..47319d2 --- /dev/null +++ b/src/components/tw-blocks/dashboard/dashboard-01/useDashboard.ts @@ -0,0 +1,119 @@ +"use client"; + +import React from "react"; +import { useWalletContext } from "../../wallet-kit/WalletProvider"; +import { useEscrowsBySignerQuery } from "../../tanstack/useEscrowsBySignerQuery"; +import type { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types"; + +type AmountsByDatePoint = { date: string; amount: number }; +type CreatedByDatePoint = { date: string; count: number }; +type DonutSlice = { type: "single" | "multi"; value: number; fill: string }; + +function getCreatedDateKey(createdAt: Escrow["createdAt"]): string { + // createdAt is a Firestore-like timestamp: { _seconds, _nanoseconds } + const seconds = (createdAt as unknown as { _seconds?: number })?._seconds; + const d = seconds ? new Date(seconds * 1000) : new Date(); + // YYYY-MM-DD + return d.toISOString().slice(0, 10); +} + +function getSingleReleaseAmount(escrow: Escrow): number { + // Single release stores total in .amount + const raw = (escrow as unknown as { amount?: number | string }).amount; + const n = Number(raw ?? 0); + return Number.isFinite(n) ? n : 0; +} + +function getMultiReleaseAmount(escrow: Escrow): number { + // Multi release accumulates across milestones + const milestones = ( + escrow as unknown as { + milestones?: Array<{ amount?: number | string }>; + } + ).milestones; + if (!Array.isArray(milestones)) return 0; + return milestones.reduce((acc: number, m) => { + const n = Number(m?.amount ?? 0); + return acc + (Number.isFinite(n) ? n : 0); + }, 0); +} + +function getEscrowAmount(escrow: Escrow): number { + if (escrow.type === "single-release") return getSingleReleaseAmount(escrow); + if (escrow.type === "multi-release") return getMultiReleaseAmount(escrow); + return 0; +} + +export function useDashboard() { + const { walletAddress } = useWalletContext(); + + const { + data = [], + isLoading, + isFetching, + isError, + refetch, + } = useEscrowsBySignerQuery({ + signer: walletAddress ?? "", + enabled: !!walletAddress, + }); + + const totalEscrows = React.useMemo(() => data.length, [data.length]); + + const totalAmount = React.useMemo(() => { + return data.reduce((acc: number, e) => acc + getEscrowAmount(e), 0); + }, [data]); + + const totalBalance = React.useMemo(() => { + return data.reduce((acc: number, e) => acc + Number(e?.balance ?? 0), 0); + }, [data]); + + const typeSlices = React.useMemo(() => { + let single = 0; + let multi = 0; + for (const e of data) { + if (e.type === "single-release") single += 1; + else if (e.type === "multi-release") multi += 1; + } + return [ + { type: "single", value: single, fill: "var(--color-single)" }, + { type: "multi", value: multi, fill: "var(--color-multi)" }, + ]; + }, [data]); + + const amountsByDate = React.useMemo(() => { + const map = new Map(); + for (const e of data) { + const key = getCreatedDateKey(e.createdAt); + const current = map.get(key) ?? 0; + map.set(key, current + getEscrowAmount(e)); + } + return Array.from(map.entries()) + .map(([date, amount]) => ({ date, amount })) + .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0)); + }, [data]); + + const createdByDate = React.useMemo(() => { + const map = new Map(); + for (const e of data) { + const key = getCreatedDateKey(e.createdAt); + map.set(key, (map.get(key) ?? 0) + 1); + } + return Array.from(map.entries()) + .map(([date, count]) => ({ date, count })) + .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0)); + }, [data]); + + return { + isLoading, + isFetching, + isError, + refetch, + totalEscrows, + totalAmount, + totalBalance, + typeSlices, + amountsByDate, + createdByDate, + } as const; +} diff --git a/src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx b/src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx new file mode 100644 index 0000000..e663288 --- /dev/null +++ b/src/components/tw-blocks/escrows/admin-analytics/AdminEscrowAnalyticsDashboard.tsx @@ -0,0 +1,339 @@ +"use client"; + +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Bar, + BarChart, + CartesianGrid, + Area, + AreaChart, + Pie, + PieChart, + XAxis, + YAxis, + Cell, +} from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + ChartLegend, + ChartLegendContent, + type ChartConfig, +} from "@/components/ui/chart"; +import { Separator } from "@/components/ui/separator"; +import { + Empty, + EmptyHeader, + EmptyMedia, + EmptyTitle, + EmptyDescription, +} from "@/components/ui/empty"; +import { useAdminEscrowAnalytics } from "./useAdminEscrowAnalytics"; +import { + Hash, + TrendingUp, + Banknote, + Coins, + RefreshCcw, + BarChart3, + Layers, + Calendar, + Loader2, + AlertCircle, + CloudOff, +} from "lucide-react"; +import { formatCurrency } from "../../helpers/format.helper"; +import { Button } from "@/components/ui/button"; + +// Config for charts +const chartConfig: ChartConfig = { + count: { + label: "Escrows", + color: "hsl(var(--chart-1))", + }, + single: { + label: "Single Release", + color: "hsl(var(--chart-1))", + }, + multi: { + label: "Multi Release", + color: "hsl(var(--chart-2))", + }, + growth: { + label: "Growth %", + color: "hsl(var(--chart-3))", + }, +}; + +export const AdminEscrowAnalyticsDashboard = ({ engagementId }: { engagementId: string }) => { + const { data, isLoading, isError, error, refetch, isFetching } = useAdminEscrowAnalytics(engagementId); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + + + + + Error loading analytics + {(error as Error)?.message} + + + + ); + } + + if (!data || data.totalEscrows === 0) { + return ( + + + + + + No data found + No escrow data available for this engagement. + + + ); + } + + const currency = data.raw[0]?.trustline?.symbol || "USDC"; + + return ( +
+
+
+

+ Escrow statistics for engagement {engagementId} +

+
+ +
+ +
+ + + Total Escrows + + + +
{data.totalEscrows}
+

+ Contracts in engagement +

+
+
+ + + + Total Amount + + + +
+ {formatCurrency(data.totalAmount, currency)} +
+

+ Sum of amounts (SR + MR) +

+
+
+ + + + Total Balance + + + +
+ {formatCurrency(data.totalBalance, currency)} +
+

+ Total remaining balance +

+
+
+ + + + MoM Growth + + + +
+ {data.growthMoM.length > 0 + ? `${data.growthMoM[data.growthMoM.length - 1].growth}%` + : "0%"} +
+

+ Escrow creation growth +

+
+
+
+ + + +
+ {/* Charts Area */} + + + + Escrows by Type + + Distribution of escrow structures + + + + + } /> + + {data.byType.map((_: unknown, index: number) => ( + + ))} + + } /> + + + + + + + + + Escrows by Date + + Trend of escrow creation over time + + + + + + value.slice(5)} // Show MM-DD + /> + + } /> + + + + + + + + + + Escrows by Date (Bar Chart) + + Daily escrow creation metrics + + + + + + value.slice(5)} + /> + + } /> + + + + + + + + + + Growth % Escrows by Month + + Month-over-month growth analytics + + + + + + + + } /> + + {data.growthMoM.map((_: unknown, index: number) => ( + + ))} + + + + + +
+
+ ); +}; diff --git a/src/components/tw-blocks/escrows/admin-analytics/aggregation.ts b/src/components/tw-blocks/escrows/admin-analytics/aggregation.ts new file mode 100644 index 0000000..d59f616 --- /dev/null +++ b/src/components/tw-blocks/escrows/admin-analytics/aggregation.ts @@ -0,0 +1,132 @@ +import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types"; +import { startOfMonth, format } from "date-fns"; + +export interface ChartDataPoint { + date: string; + count: number; +} + +export interface TypeDataPoint { + type: string; + count: number; + fill?: string; +} + +export interface GrowthDataPoint { + month: string; + count: number; + growth: number; +} + +/** + * Groups escrows by their type (single-release vs multi-release) + */ +export const groupEscrowsByType = (escrows: Escrow[]): TypeDataPoint[] => { + const types = escrows.reduce((acc, escrow) => { + const type = escrow.type || "unknown"; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + return Object.entries(types).map(([type, count]) => ({ + type: type === "single-release" ? "Single Release" : "Multi Release", + count, + })); +}; + +/** + * Groups escrows by creation date (YYYY-MM-DD) for trend analysis + */ +export const groupEscrowsByDate = (escrows: Escrow[]): ChartDataPoint[] => { + const groups = escrows.reduce((acc, escrow) => { + const date = new Date(escrow.createdAt._seconds * 1000); + const dateKey = format(date, "yyyy-MM-dd"); + acc[dateKey] = (acc[dateKey] || 0) + 1; + return acc; + }, {} as Record); + + return Object.entries(groups) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, count]) => ({ + date, + count, + })); +}; + +/** + * Calculates month-over-month growth percentages + */ +export const calculateMoMGrowth = (escrows: Escrow[]): GrowthDataPoint[] => { + const months: Record = {}; + + // Sort escrows by date + const sortedEscrows = [...escrows].sort((a, b) => a.createdAt._seconds - b.createdAt._seconds); + + if (sortedEscrows.length === 0) return []; + + // Get range of months + const firstDate = new Date(sortedEscrows[0].createdAt._seconds * 1000); + const lastDate = new Date(sortedEscrows[sortedEscrows.length - 1].createdAt._seconds * 1000); + + let current = startOfMonth(firstDate); + const end = startOfMonth(lastDate); + + while (current <= end) { + const monthKey = format(current, "yyyy-MM"); + months[monthKey] = 0; + current = startOfMonth(new Date(current.setMonth(current.getMonth() + 1))); + } + + // Count escrows per month + sortedEscrows.forEach(escrow => { + const date = new Date(escrow.createdAt._seconds * 1000); + const monthKey = format(date, "yyyy-MM"); + if (months[monthKey] !== undefined) { + months[monthKey]++; + } + }); + + const monthEntries = Object.entries(months).sort(([a], [b]) => a.localeCompare(b)); + + return monthEntries.map(([month, count], index) => { + let growth = 0; + if (index > 0) { + const prevCount = monthEntries[index - 1][1]; + if (prevCount === 0) { + growth = count > 0 ? 100 : 0; + } else { + growth = ((count - prevCount) / prevCount) * 100; + } + } + + return { + month: format(new Date(month + "-01"), "MMM yy"), + count, + growth: Math.round(growth * 100) / 100, + }; + }); +}; + +/** + * Calculates the sum of amounts across all escrows (SR + MR) + */ +export const calculateTotalAmount = (escrows: Escrow[]): number => { + return escrows.reduce((acc, escrow) => { + if (escrow.type === "single-release") { + return acc + (escrow.amount || 0); + } + // For multi-release, amount is the sum of milestones + const milestoneSum = escrow.milestones?.reduce( + (mAcc, m) => mAcc + ((m as { amount?: number }).amount || 0), + 0 + ) || 0; + return acc + milestoneSum; + }, 0); +}; + +/** + * Calculates the total balance across all escrows + */ +export const calculateTotalBalance = (escrows: Escrow[]): number => { + return escrows.reduce((acc, escrow) => acc + (escrow.balance || 0), 0); +}; diff --git a/src/components/tw-blocks/escrows/admin-analytics/index.ts b/src/components/tw-blocks/escrows/admin-analytics/index.ts new file mode 100644 index 0000000..f0da412 --- /dev/null +++ b/src/components/tw-blocks/escrows/admin-analytics/index.ts @@ -0,0 +1,3 @@ +export * from "./AdminEscrowAnalyticsDashboard"; +export * from "./useAdminEscrowAnalytics"; +export * from "./aggregation"; diff --git a/src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts b/src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts new file mode 100644 index 0000000..caf11fa --- /dev/null +++ b/src/components/tw-blocks/escrows/admin-analytics/useAdminEscrowAnalytics.ts @@ -0,0 +1,98 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { + useGetEscrowsFromIndexerBySigner, + useGetEscrowFromIndexerByContractIds +} from "@trustless-work/escrow/hooks"; +import { GetEscrowsFromIndexerResponse as Escrow } from "@trustless-work/escrow/types"; +import { useWalletContext } from "../../wallet-kit/WalletProvider"; +import { + groupEscrowsByType, + groupEscrowsByDate, + calculateMoMGrowth, + calculateTotalAmount, + calculateTotalBalance +} from "./aggregation"; + +export interface AdminAnalyticsData { + totalEscrows: number; + totalAmount: number; + totalBalance: number; + byType: ReturnType; + byDate: ReturnType; + growthMoM: ReturnType; + raw: Escrow[]; +} + +export const useAdminEscrowAnalytics = (engagementId: string) => { + const { walletAddress } = useWalletContext(); + const { getEscrowsBySigner } = useGetEscrowsFromIndexerBySigner(); + const { getEscrowByContractIds } = useGetEscrowFromIndexerByContractIds(); + + const query = useQuery({ + queryKey: ["adminEscrowAnalytics", engagementId, walletAddress], + enabled: !!engagementId && !!walletAddress, + queryFn: async (): Promise => { + if (!walletAddress) throw new Error("Wallet not connected"); + + //Get list of escrows for the engagement + const list = await getEscrowsBySigner({ + signer: walletAddress, + engagementId, + }); + + if (!list || list.length === 0) { + return { + totalEscrows: 0, + totalAmount: 0, + totalBalance: 0, + byType: [], + byDate: [], + growthMoM: [], + raw: [], + }; + } + + //Mandated hook: useGetEscrowFromIndexerByContractIds + const contractIds = list + .map((e) => e.contractId) + .filter((id): id is string => !!id); + + if (contractIds.length === 0) { + return { + totalEscrows: 0, + totalAmount: 0, + totalBalance: 0, + byType: [], + byDate: [], + growthMoM: [], + raw: [], + }; + } + + const detailedEscrows = await getEscrowByContractIds({ + contractIds, + validateOnChain: false, + }); + + if (!detailedEscrows) { + throw new Error("Failed to fetch detailed escrows"); + } + + // Step 3: Aggregation + return { + totalEscrows: detailedEscrows.length, + totalAmount: calculateTotalAmount(detailedEscrows), + totalBalance: calculateTotalBalance(detailedEscrows), + byType: groupEscrowsByType(detailedEscrows), + byDate: groupEscrowsByDate(detailedEscrows), + growthMoM: calculateMoMGrowth(detailedEscrows), + raw: detailedEscrows, + }; + }, + staleTime: 1000 * 60 * 5, + }); + + return query; +};