Initial Commit

This commit is contained in:
Huzaifa Inam 2026-08-07 13:06:39 +05:00
commit 541f2ac0c0
31 changed files with 13917 additions and 0 deletions

9
.env.example Normal file
View file

@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

20
README.md Normal file
View file

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/1d635da8-bc7b-4711-8a8b-119cb50ea598
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

765
bun.lock Normal file
View file

@ -0,0 +1,765 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "react-example",
"dependencies": {
"@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14",
"@types/three": "^0.185.4",
"@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"nodemailer": "^9.0.4",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"recharts": "^3.10.1",
"three": "^0.185.1",
"vite": "^6.2.3",
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@google/genai": ["@google/genai@2.15.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-Q41TvqwBQ9NcmWdh6qxY5qrpg+0FaVHD7febQoH007pykxzco4ohScBUP4BBBy+Q8j5D8euIBSRIBDfWuNVCKA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.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-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@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/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/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@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-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@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/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
"@types/three": ["@types/three@0.185.4", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
"autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="],
"browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"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=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"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-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-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=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.401", "", {}, "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gaxios": ["gaxios@7.3.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"immer": ["immer@11.1.15", "", {}, "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"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=="],
"motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="],
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-releases": ["node-releases@2.0.52", "", {}, "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A=="],
"nodemailer": ["nodemailer@9.0.4", "", {}, "sha512-LmJNRVRtfSCULxcZpy0Cpg4WWenlUZ9+zbmTO+S7v9wD6XreYLjXRFtDjtV/4F0HT5p1GyZfA0Ux/myxHb18CQ=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"react-is": ["react-is@19.2.8", "", {}, "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ=="],
"react-redux": ["react-redux@9.3.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-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"recharts": ["recharts@3.10.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "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-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA=="],
"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=="],
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.23.7", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-3f/u/+UDCNQ7iwUZW9FCMnNGIHzElGJYh0S/yy8IvWSsn5O7fEO/897FaG7FA2W8yryiRyuwXZ1PYLAKYaqSuQ=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"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=="],
"vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" }, "bundled": true }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
}
}

13
index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6
metadata.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "International Network Balance Sheet Portal",
"description": "Multi-branch Balance Sheet portal for international banking networks with variance tracking, auto-checks, reconciliation enforcement, and consolidated view.",
"requestFramePermissions": [],
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
}

41
package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "tsx server.ts",
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
"start": "node dist/server.cjs",
"preview": "vite preview",
"clean": "rm -rf dist server.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14",
"@types/three": "^0.185.4",
"@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"nodemailer": "^9.0.4",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"recharts": "^3.10.1",
"three": "^0.185.1",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}

0
portal-data.json Normal file
View file

1569
server.ts Normal file

File diff suppressed because it is too large Load diff

225
src/App.tsx Normal file
View file

@ -0,0 +1,225 @@
import React, { useState, useEffect } from 'react';
import { Sidebar, HeaderBar } from './components/Navbar';
import { ConsolidatedView } from './components/ConsolidatedView';
import { BranchPortalView } from './components/BranchPortalView';
import { InvestmentsPortfolioView } from './components/InvestmentsPortfolioView';
import { PlacementsBorrowingsView } from './components/PlacementsBorrowingsView';
import { LiquidityGapView } from './components/LiquidityGapView';
import { AutoChecksView } from './components/AutoChecksView';
import { DashboardAnalytics } from './components/DashboardAnalytics';
import { AdminUserManagement } from './components/AdminUserManagement';
import { AuthModal } from './components/AuthModal';
import { HolographicCanvas, HolographicThemeMode } from './components/HolographicCanvas';
import { User, BranchSubmission, BranchId } from './types';
import { INITIAL_USERS } from './data/seedData';
export default function App() {
const [currentUser, setCurrentUser] = useState<User>(INITIAL_USERS[0]); // Default Head Office Admin
const [activeTab, setActiveTab] = useState<'consolidated' | 'branch_input' | 'placements_borrowings' | 'investments' | 'liquidity_gap' | 'autochecks' | 'analytics' | 'admin'>('consolidated');
const [activePeriod, setActivePeriod] = useState<string>('22-May-26');
const [priorPeriod, setPriorPeriod] = useState<string>('15-May-26');
const [periodsList, setPeriodsList] = useState<string[]>(['22-May-26', '15-May-26', '08-May-26', '01-May-26']);
const [themeMode, setThemeMode] = useState<HolographicThemeMode>('cyan');
const [isAuthModalOpen, setIsAuthModalOpen] = useState<boolean>(false);
const [submissions, setSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
const [priorSubmissions, setPriorSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
const [allUsers, setAllUsers] = useState<User[]>(INITIAL_USERS);
const [loading, setLoading] = useState<boolean>(true);
// Auto switch tab if branch user logs in and is on admin-only view
useEffect(() => {
if (currentUser.role === 'branch_user') {
if (activeTab === 'consolidated' || activeTab === 'autochecks' || activeTab === 'admin' || activeTab === 'analytics') {
setActiveTab('branch_input');
}
}
}, [currentUser]);
// Fetch submissions for active period and prior period
const loadPortalData = async () => {
setLoading(true);
try {
const [pRes, sRes, uRes] = await Promise.all([
fetch('/api/periods'),
fetch(`/api/balancesheets/${activePeriod}`),
fetch('/api/auth/users'),
]);
const pData = await pRes.json();
const sData = await sRes.json();
const uData = await uRes.json();
if (pData.periods) setPeriodsList(pData.periods);
if (sData.submissions) setSubmissions(sData.submissions);
if (sData.priorSubmissions) setPriorSubmissions(sData.priorSubmissions);
if (sData.priorPeriod) setPriorPeriod(sData.priorPeriod);
if (uData.users) setAllUsers(uData.users);
} catch (err) {
console.error('Error fetching portal data:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPortalData();
}, [activePeriod]);
// Handle resetting baseline seed data
const handleResetSeed = async () => {
if (window.confirm('Reset all balance sheet submissions back to initial baseline data?')) {
try {
await fetch('/api/reset-seed', { method: 'POST' });
await loadPortalData();
} catch (err) {
console.error('Error resetting seed:', err);
}
}
};
return (
<div className="relative h-screen w-screen bg-slate-950 text-slate-100 flex flex-col md:flex-row overflow-hidden font-sans antialiased selection:bg-cyan-500 selection:text-black">
{/* 3D WebGL Holographic Particle Canvas Background */}
<HolographicCanvas themeMode={themeMode} onThemeChange={setThemeMode} />
{/* Left Navigation Sidebar for Desktop & Tablet */}
<Sidebar
currentUser={currentUser}
setCurrentUser={setCurrentUser}
activeTab={activeTab}
setActiveTab={setActiveTab}
allUsers={allUsers}
onOpenAuthModal={() => setIsAuthModalOpen(true)}
/>
{/* Main Glassmorphic Workspace Container */}
<div className="relative z-10 flex-1 flex flex-col h-full overflow-hidden min-w-0">
{/* Top Header Bar */}
<HeaderBar
currentUser={currentUser}
setCurrentUser={setCurrentUser}
activeTab={activeTab}
setActiveTab={setActiveTab}
activePeriod={activePeriod}
setActivePeriod={setActivePeriod}
periodsList={periodsList}
allUsers={allUsers}
onResetSeed={handleResetSeed}
onOpenAuthModal={() => setIsAuthModalOpen(true)}
/>
{/* Scrollable View Area */}
<main className="flex-1 overflow-y-auto p-3 sm:p-5 space-y-6">
{loading ? (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<div className="relative w-14 h-14">
<div className="absolute inset-0 rounded-full border-2 border-cyan-500/30 animate-ping"></div>
<div className="w-14 h-14 border-4 border-cyan-400 border-t-transparent rounded-full animate-spin"></div>
</div>
<p className="text-xs text-cyan-300 font-mono tracking-widest uppercase glow-text-cyan animate-pulse">
INITIALIZING HOLOGRAPHIC QUANTUM MATRIX...
</p>
</div>
) : (
<>
{activeTab === 'consolidated' && (
<ConsolidatedView
period={activePeriod}
priorPeriod={priorPeriod}
submissions={submissions}
priorSubmissions={priorSubmissions}
currentUser={currentUser}
/>
)}
{activeTab === 'branch_input' && (
<BranchPortalView
currentUser={currentUser}
period={activePeriod}
priorPeriod={priorPeriod}
submissions={submissions}
priorSubmissions={priorSubmissions}
onSubmitSuccess={loadPortalData}
/>
)}
{activeTab === 'placements_borrowings' && (
<PlacementsBorrowingsView
currentUser={currentUser}
activePeriod={activePeriod}
/>
)}
{activeTab === 'investments' && (
<InvestmentsPortfolioView
currentUser={currentUser}
activePeriod={activePeriod}
/>
)}
{activeTab === 'liquidity_gap' && (
<LiquidityGapView
currentUser={currentUser}
activePeriod={activePeriod}
/>
)}
{activeTab === 'autochecks' && (
<AutoChecksView period={activePeriod} />
)}
{activeTab === 'analytics' && (
<DashboardAnalytics period={activePeriod} submissions={submissions} />
)}
{activeTab === 'admin' && (
<AdminUserManagement onUsersUpdated={loadPortalData} />
)}
</>
)}
</main>
{/* Floating Futuristic Status Footer */}
<footer className="bg-slate-900/90 backdrop-blur-xl border-t border-cyan-500/30 py-2.5 px-6 text-[11px] text-slate-300 flex flex-wrap justify-between items-center font-mono shrink-0 relative z-20 shadow-[0_-4px_20px_rgba(0,0,0,0.4)]">
<div className="flex flex-wrap items-center gap-4">
<span className="text-cyan-400 font-bold flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-ping"></span>
TRACE ID #002934-C
</span>
<span className="text-slate-700 hidden sm:inline">|</span>
<span className="text-emerald-400 font-medium flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"></span>
Placement vs Borrowing Equality Verified (0.00 Mn Delta)
</span>
</div>
<div className="flex items-center gap-4 mt-1 sm:mt-0">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-gradient-to-r from-cyan-950/80 via-indigo-950/80 to-slate-900 border border-cyan-500/50 text-cyan-200 font-sans shadow-[0_0_15px_rgba(6,182,212,0.2)]">
<span className="text-[10px] text-cyan-400 uppercase tracking-widest font-mono font-bold">Powered by</span>
<span className="text-xs font-black bg-gradient-to-r from-cyan-300 via-indigo-200 to-white bg-clip-text text-transparent">
Huzaifa Inam
</span>
</div>
<div className="text-slate-400 hidden md:flex items-center gap-3 text-[10px]">
<span className="text-cyan-300/80">3D WebGL Engine Active</span>
<span>v2.5.0 Quantum Build</span>
</div>
</div>
</footer>
</div>
{/* Authentication & User Switching Modal */}
<AuthModal
isOpen={isAuthModalOpen}
onClose={() => setIsAuthModalOpen(false)}
currentUser={currentUser}
setCurrentUser={setCurrentUser}
allUsers={allUsers}
onLoginSuccess={loadPortalData}
/>
</div>
);
}

View file

@ -0,0 +1,591 @@
import React, { useState, useEffect } from 'react';
import {
Users,
Mail,
ShieldCheck,
CheckCircle2,
XCircle,
Send,
UserPlus,
Settings,
Clock,
RefreshCw,
AlertCircle,
Key,
Lock
} from 'lucide-react';
import { User, SmtpConfig, EmailLog, BRANCHES_LIST, BranchId } from '../types';
interface AdminUserManagementProps {
onUsersUpdated: () => void;
}
export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUsersUpdated }) => {
const [users, setUsers] = useState<User[]>([]);
const [smtpConfig, setSmtpConfig] = useState<SmtpConfig>({
host: 'smtp.networkbank.com',
port: 587,
username: 'alerts@networkbank.com',
fromEmail: 'noreply-portal@networkbank.com',
useTls: true,
autoRemindersEnabled: true,
reminderFrequencyDays: 7,
});
const [emailLogs, setEmailLogs] = useState<EmailLog[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [reminderSending, setReminderSending] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<string | null>(null);
// FX Rates State (Head Office PKR Rates)
const [pkrRates, setPkrRates] = useState<Record<string, number>>({
USDPKR: 278.16,
EURPKR: 302.35,
GBPPKR: 356.60,
JPYPKR: 1.79,
AEDPKR: 75.79,
SARPKR: 74.18,
BDTPKR: 2.36,
CNYPKR: 38.47,
HKDPKR: 35.57,
CADPKR: 204.53,
});
const [savingFx, setSavingFx] = useState<boolean>(false);
// New User Form State
const [newEmail, setNewEmail] = useState('');
const [newName, setNewName] = useState('');
const [newBranchId, setNewBranchId] = useState<BranchId>('bahrain');
const fetchData = async () => {
setLoading(true);
try {
const [uRes, sRes, fxRes] = await Promise.all([
fetch('/api/auth/users'),
fetch('/api/smtp/config'),
fetch('/api/fx-rates'),
]);
const uData = await uRes.json();
const sData = await sRes.json();
const fxData = fxRes.ok ? await fxRes.json() : null;
if (uData.users) setUsers(uData.users);
if (sData.config) setSmtpConfig(sData.config);
if (sData.logs) setEmailLogs(sData.logs);
if (fxData && fxData.pkrRates) setPkrRates(fxData.pkrRates);
} catch (err) {
console.error('Error loading admin settings:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
// Approve or Reject user
const handleApprove = async (userId: string, approve: boolean) => {
try {
const res = await fetch('/api/auth/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, approve }),
});
const data = await res.json();
if (data.users) setUsers(data.users);
setStatusMsg(`User ${approve ? 'approved' : 'rejected/removed'} successfully.`);
onUsersUpdated();
} catch (err) {
console.error('Error updating user approval status:', err);
}
};
// Register new branch user
const handleRegisterUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!newEmail || !newName) return;
try {
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: newEmail,
name: newName,
branchId: newBranchId,
role: 'branch_user',
}),
});
const data = await res.json();
if (res.ok) {
setStatusMsg(`User ${newName} registered. Waiting for admin approval.`);
setNewEmail('');
setNewName('');
fetchData();
} else {
setStatusMsg(data.error || 'Registration failed.');
}
} catch (err) {
console.error('Registration error:', err);
}
};
// Force Reset Password for branch account
const handleForceResetPassword = async (userId: string, targetName: string) => {
setStatusMsg(null);
try {
const res = await fetch('/api/auth/force-reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, mustChangePassword: true, temporaryPassword: 'password123' }),
});
const data = await res.json();
if (res.ok) {
if (data.users) setUsers(data.users);
setStatusMsg(`Mandatory password reset enforced for ${targetName}. Temporary pass set to 'password123'.`);
onUsersUpdated();
} else {
setStatusMsg(`Error resetting password: ${data.error}`);
}
} catch (err) {
console.error('Error in force reset password:', err);
}
};
// Save SMTP Settings
const handleSaveSmtp = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await fetch('/api/smtp/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(smtpConfig),
});
if (res.ok) {
setStatusMsg('SMTP Server Configuration saved successfully.');
}
} catch (err) {
console.error('Error saving SMTP config:', err);
}
};
// Send Reminders button
const handleSendReminders = async () => {
setReminderSending(true);
setStatusMsg(null);
try {
const res = await fetch('/api/smtp/send-reminders', { method: 'POST' });
const data = await res.json();
if (data.logs) setEmailLogs(data.logs);
setStatusMsg(data.message || 'Reminders dispatched successfully.');
} catch (err) {
console.error('Error sending reminders:', err);
} finally {
setReminderSending(false);
}
};
// Save HO FX Rates
const handleSaveFxRates = async (e: React.FormEvent) => {
e.preventDefault();
setSavingFx(true);
setStatusMsg(null);
try {
const res = await fetch('/api/fx-rates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pkrRates }),
});
if (res.ok) {
setStatusMsg('Head Office FX Rates saved. USD cross rates automatically derived.');
}
} catch (err) {
console.error('Error saving FX rates:', err);
} finally {
setSavingFx(false);
}
};
const usdPkr = pkrRates.USDPKR || 278.16;
const FX_CURRENCIES = [
{ key: 'USDPKR', name: 'US Dollar', code: 'USD', symbol: '$' },
{ key: 'EURPKR', name: 'Euro', code: 'EUR', symbol: '€' },
{ key: 'GBPPKR', name: 'British Pound', code: 'GBP', symbol: '£' },
{ key: 'JPYPKR', name: 'Japanese Yen', code: 'JPY', symbol: '¥' },
{ key: 'AEDPKR', name: 'UAE Dirham', code: 'AED', symbol: 'AED' },
{ key: 'SARPKR', name: 'Saudi Riyal', code: 'SAR', symbol: 'SAR' },
{ key: 'BDTPKR', name: 'Bangladeshi Taka', code: 'BDT', symbol: '৳' },
{ key: 'CNYPKR', name: 'Chinese Yuan', code: 'CNY', symbol: '¥' },
{ key: 'HKDPKR', name: 'Hong Kong Dollar', code: 'HKD', symbol: 'HK$' },
{ key: 'CADPKR', name: 'Canadian Dollar', code: 'CAD', symbol: 'CA$' },
];
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Top Banner */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-3">
<div className="p-3 bg-amber-600/20 border border-amber-500/30 rounded-xl text-amber-400">
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<h2 className="text-lg font-bold text-white">Head Office Admin & SMTP Control</h2>
<p className="text-xs text-slate-400">
Manage branch login approvals, user permissions, and SMTP automated submission notifications.
</p>
</div>
</div>
<button
onClick={handleSendReminders}
disabled={reminderSending}
className="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white font-bold rounded-lg text-xs flex items-center space-x-2 shadow-lg transition-all cursor-pointer"
>
{reminderSending ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
<span>Send Reminders to Pending Branches</span>
</button>
</div>
{statusMsg && (
<div className="bg-blue-950 border border-blue-500/60 text-blue-200 p-3.5 rounded-xl text-xs font-medium flex items-center space-x-2">
<CheckCircle2 className="w-4 h-4 text-blue-400 shrink-0" />
<span>{statusMsg}</span>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* User Accounts & Approvals Panel */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Users className="w-4 h-4 text-blue-400" />
Branch Login Credentials & Approvals
</h3>
<span className="text-xs font-mono text-slate-400">{users.length} Users</span>
</div>
{/* Users List */}
<div className="space-y-2 max-h-80 overflow-y-auto pr-1">
{users.map((u) => {
const uBranch = BRANCHES_LIST.find((b) => b.id === u.branchId);
return (
<div
key={u.id}
className="p-3 bg-slate-950 border border-slate-800 rounded-lg flex items-center justify-between text-xs"
>
<div>
<div className="font-bold text-slate-100 flex items-center gap-1.5 flex-wrap">
{u.name}
{u.role === 'admin' && (
<span className="text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-1.5 rounded font-mono">
Admin
</span>
)}
{u.approved ? (
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-1.5 rounded font-mono">
Approved
</span>
) : (
<span className="text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-1.5 rounded animate-pulse font-mono">
Pending Approval
</span>
)}
{u.role !== 'admin' && (
u.mustChangePassword ? (
<span className="text-[9px] bg-rose-500/20 text-rose-300 border border-rose-500/40 px-1.5 rounded font-mono flex items-center gap-1">
<Key className="w-2.5 h-2.5 text-rose-400" />
Reset Required
</span>
) : u.isPasswordChanged ? (
<span className="text-[9px] bg-cyan-500/20 text-cyan-300 border border-cyan-500/40 px-1.5 rounded font-mono flex items-center gap-1">
<Lock className="w-2.5 h-2.5 text-cyan-400" />
Custom Pass
</span>
) : (
<span className="text-[9px] bg-slate-800 text-slate-400 border border-slate-700 px-1.5 rounded font-mono">
Default Pass (password123)
</span>
)
)}
</div>
<div className="text-[11px] text-slate-400 font-mono mt-0.5">{u.email} ({uBranch?.name || 'Head Office'})</div>
</div>
{u.role !== 'admin' && (
<div className="flex items-center space-x-1 shrink-0 ml-2">
{u.approved && (
<button
onClick={() => handleForceResetPassword(u.id, u.name)}
className="px-2 py-1 bg-amber-950/60 hover:bg-amber-900 border border-amber-500/40 text-amber-300 font-semibold rounded text-[10px] flex items-center gap-1 transition-colors cursor-pointer"
title="Enforce mandatory password change on user's next login"
>
<Key className="w-3 h-3 text-amber-400" />
<span>Force Reset</span>
</button>
)}
{!u.approved && (
<button
onClick={() => handleApprove(u.id, true)}
className="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
>
<CheckCircle2 className="w-3 h-3" />
Approve
</button>
)}
<button
onClick={() => handleApprove(u.id, false)}
className="px-2.5 py-1 bg-slate-800 hover:bg-rose-900 text-slate-300 hover:text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
>
<XCircle className="w-3 h-3" />
Remove
</button>
</div>
)}
</div>
);
})}
</div>
{/* Add New User / Branch Credential Form */}
<form onSubmit={handleRegisterUser} className="bg-slate-950/80 p-4 rounded-xl border border-slate-800 space-y-3 pt-4">
<h4 className="text-xs font-bold text-slate-300 flex items-center gap-1.5">
<UserPlus className="w-4 h-4 text-blue-400" />
Register New Branch User Credential
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs">
<input
type="text"
placeholder="User Full Name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="bg-slate-900 border border-slate-700 rounded px-2.5 py-1.5 text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
required
/>
<input
type="email"
placeholder="branch@networkbank.com"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
className="bg-slate-900 border border-slate-700 rounded px-2.5 py-1.5 text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
required
/>
</div>
<div className="flex justify-between items-center text-xs">
<select
value={newBranchId}
onChange={(e) => setNewBranchId(e.target.value as BranchId)}
className="bg-slate-900 border border-slate-700 text-slate-200 rounded px-2.5 py-1.5 focus:outline-none"
>
{BRANCHES_LIST.map((b) => (
<option key={b.id} value={b.id}>
{b.name} ({b.code})
</option>
))}
</select>
<button
type="submit"
className="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 text-white font-bold rounded text-xs transition-colors"
>
Register & Approve
</button>
</div>
</form>
</div>
{/* SMTP Configuration Panel */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Mail className="w-4 h-4 text-amber-400" />
SMTP Server & Automated Notifications
</h3>
<span className="text-xs font-mono text-emerald-400">SMTP Ready</span>
</div>
<form onSubmit={handleSaveSmtp} className="space-y-3 text-xs">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-slate-400 block mb-1 font-semibold">SMTP Host Server</label>
<input
type="text"
value={smtpConfig.host}
onChange={(e) => setSmtpConfig({ ...smtpConfig, host: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
<div>
<label className="text-slate-400 block mb-1 font-semibold">Port</label>
<input
type="number"
value={smtpConfig.port}
onChange={(e) => setSmtpConfig({ ...smtpConfig, port: Number(e.target.value) })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-slate-400 block mb-1 font-semibold">From Sender Email</label>
<input
type="email"
value={smtpConfig.fromEmail}
onChange={(e) => setSmtpConfig({ ...smtpConfig, fromEmail: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
<div>
<label className="text-slate-400 block mb-1 font-semibold">Username / Service Account</label>
<input
type="text"
value={smtpConfig.username}
onChange={(e) => setSmtpConfig({ ...smtpConfig, username: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-800">
<label className="flex items-center space-x-2 cursor-pointer text-slate-300">
<input
type="checkbox"
checked={smtpConfig.useTls}
onChange={(e) => setSmtpConfig({ ...smtpConfig, useTls: e.target.checked })}
className="rounded text-amber-500 focus:ring-0"
/>
<span>Enable TLS / SSL Secure Enclosure</span>
</label>
<button
type="submit"
className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-white font-bold rounded text-xs transition-colors"
>
Save SMTP Config
</button>
</div>
</form>
{/* Email Audit Log */}
<div className="pt-2 border-t border-slate-800 space-y-2">
<h4 className="text-xs font-bold text-slate-300 flex items-center justify-between">
<span>Dispatched Notification Logs</span>
<span className="text-[10px] text-slate-400 font-mono">{emailLogs.length} Entries</span>
</h4>
<div className="space-y-1.5 max-h-44 overflow-y-auto pr-1 text-[11px] font-mono">
{emailLogs.map((log) => (
<div key={log.id} className="p-2 bg-slate-950 border border-slate-800 rounded space-y-0.5">
<div className="flex justify-between items-center text-slate-300">
<span className="font-bold text-amber-300 truncate max-w-[200px]">{log.recipientEmail}</span>
<span className="text-[10px] text-slate-500">{new Date(log.sentAt).toLocaleTimeString()}</span>
</div>
<div className="text-[10px] text-slate-400 truncate">{log.subject}</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* HO FOREIGN EXCHANGE RATES CONTROL PANEL */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-800 pb-3">
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wider font-mono flex items-center gap-2">
<span className="text-emerald-400 font-extrabold">$</span>
Head Office Foreign Exchange Rates Management
</h3>
<p className="text-xs text-slate-400 mt-0.5">
Enter official Head Office exchange rates against PKR. Cross rates in USD are automatically derived (<span className="text-cyan-300 font-mono">1 USD = USDPKR / XPKR</span>).
</p>
</div>
<button
onClick={handleSaveFxRates}
disabled={savingFx}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-lg transition-all cursor-pointer font-mono"
>
{savingFx ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Settings className="w-4 h-4" />}
<span>Save & Apply FX Rates</span>
</button>
</div>
<form onSubmit={handleSaveFxRates}>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs font-mono border-collapse">
<thead>
<tr className="bg-slate-950 text-slate-400 border-b border-slate-800 text-[10px] uppercase tracking-wider">
<th className="p-2.5">Currency Pair</th>
<th className="p-2.5">Currency Name</th>
<th className="p-2.5 text-right">HO PKR Rate (PKR per 1 Unit)</th>
<th className="p-2.5 text-right">Derived USD Cross Rate (1 USD = X Units)</th>
<th className="p-2.5 text-right">USD Equiv per 100 Units</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/80">
{FX_CURRENCIES.map((curr) => {
const val = pkrRates[curr.key] || 0;
const crossUsd = val > 0 ? (usdPkr / val) : 0;
const usdPer100 = val > 0 ? (val / usdPkr * 100) : 0;
return (
<tr key={curr.key} className="hover:bg-slate-800/50 transition-colors">
<td className="p-2.5 font-bold text-amber-300 flex items-center gap-1.5">
<span className="w-6 h-6 rounded-md bg-slate-800 border border-slate-700 flex items-center justify-center text-[10px] font-bold text-slate-200">
{curr.code}
</span>
<span>{curr.key}</span>
</td>
<td className="p-2.5 text-slate-300">
{curr.name} ({curr.symbol})
</td>
<td className="p-2.5 text-right">
<div className="flex items-center justify-end gap-1.5">
<span className="text-[10px] text-slate-500 font-semibold">PKR</span>
<input
type="number"
step="0.0001"
value={pkrRates[curr.key] ?? ''}
onChange={(e) =>
setPkrRates({
...pkrRates,
[curr.key]: parseFloat(e.target.value) || 0,
})
}
className="w-32 bg-slate-950 border border-slate-700 rounded-lg p-1.5 text-right text-emerald-300 font-bold focus:outline-none focus:border-cyan-400 font-mono text-xs"
/>
</div>
</td>
<td className="p-2.5 text-right font-bold text-cyan-300">
{curr.code === 'USD' ? '1.0000' : crossUsd.toFixed(4)} <span className="text-[10px] text-slate-500 font-normal">{curr.code}</span>
</td>
<td className="p-2.5 text-right font-bold text-slate-200">
${usdPer100.toFixed(2)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</form>
</div>
</div>
);
};

View file

@ -0,0 +1,547 @@
import React, { useState } from 'react';
import { User, BranchId, BRANCHES_LIST } from '../types';
import {
ShieldCheck,
Building2,
Lock,
Mail,
Key,
CheckCircle2,
X,
LogIn,
Shield,
Eye,
EyeOff,
RefreshCw,
AlertTriangle,
ArrowRight,
RotateCcw,
Send,
HelpCircle
} from 'lucide-react';
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
currentUser: User;
setCurrentUser: (user: User) => void;
allUsers: User[];
onLoginSuccess?: () => void;
}
export const AuthModal: React.FC<AuthModalProps> = ({
isOpen,
onClose,
currentUser,
setCurrentUser,
allUsers,
onLoginSuccess
}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [selectedBranchId, setSelectedBranchId] = useState<BranchId | 'head_office'>('head_office');
const [showPassword, setShowPassword] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
const [loginSuccess, setLoginSuccess] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Forced password change state
const [mustChangeMode, setMustChangeMode] = useState(false);
const [pendingUser, setPendingUser] = useState<User | null>(null);
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [changePassError, setChangePassError] = useState<string | null>(null);
// Forgot Password state
const [isForgotPassMode, setIsForgotPassMode] = useState(false);
const [forgotEmail, setForgotEmail] = useState('');
const [forgotStatus, setForgotStatus] = useState<{ message: string; userEmail?: string } | null>(null);
const [forgotError, setForgotError] = useState<string | null>(null);
if (!isOpen) return null;
// Perform credential login via backend authentication
const handleCredentialLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError(null);
setLoginSuccess(null);
if (!email.trim()) {
setLoginError('Email / Username is required.');
return;
}
if (!password) {
setLoginError('Password authentication is required.');
return;
}
setIsLoading(true);
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
setLoginError(data.error || 'Authentication failed. Please verify credentials.');
setIsLoading(false);
return;
}
const authenticatedUser: User = data.user;
// Check if user is required to set new password on first login or forced reset
if (data.mustChangePassword || authenticatedUser.mustChangePassword) {
setPendingUser(authenticatedUser);
setMustChangeMode(true);
setLoginSuccess(`Credentials verified for ${authenticatedUser.name}. Mandatory password update required.`);
setIsLoading(false);
return;
}
setCurrentUser(authenticatedUser);
setLoginSuccess(
`Authenticated successfully as ${authenticatedUser.name} (${
authenticatedUser.role === 'admin' ? 'Head Office Admin' : authenticatedUser.branchId?.toUpperCase()
})`
);
setTimeout(() => {
setIsLoading(false);
onLoginSuccess?.();
onClose();
}, 700);
} catch (err) {
console.error('Login error:', err);
setLoginError('Server connection error. Please try again.');
setIsLoading(false);
}
};
// Submit Forgot Password request
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault();
setForgotError(null);
setForgotStatus(null);
if (!forgotEmail || !forgotEmail.trim()) {
setForgotError('Please enter your registered email address.');
return;
}
setIsLoading(true);
try {
const res = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: forgotEmail.trim() }),
});
const data = await res.json();
if (!res.ok) {
setForgotError(data.error || 'Failed to process password reset request.');
setIsLoading(false);
return;
}
setForgotStatus({
message: data.message,
userEmail: data.userEmail,
});
setIsLoading(false);
} catch (err) {
console.error('Forgot password error:', err);
setForgotError('Failed to connect to authentication server.');
setIsLoading(false);
}
};
// Submit new forced password
const handleSetNewPassword = async (e: React.FormEvent) => {
e.preventDefault();
setChangePassError(null);
if (!newPassword || newPassword.length < 4) {
setChangePassError('New password must be at least 4 characters long.');
return;
}
if (newPassword !== confirmPassword) {
setChangePassError('Passwords do not match. Please re-enter.');
return;
}
if (!pendingUser) return;
setIsLoading(true);
try {
const res = await fetch('/api/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: pendingUser.id,
email: pendingUser.email,
newPassword: newPassword,
}),
});
const data = await res.json();
if (!res.ok) {
setChangePassError(data.error || 'Failed to update password.');
setIsLoading(false);
return;
}
const updatedUser: User = data.user;
setCurrentUser(updatedUser);
setLoginSuccess(`New password saved successfully! Logging in as ${updatedUser.name}...`);
setTimeout(() => {
setIsLoading(false);
setMustChangeMode(false);
setPendingUser(null);
onLoginSuccess?.();
onClose();
}, 800);
} catch (err) {
console.error('Password change error:', err);
setChangePassError('Failed to change password. Server error.');
setIsLoading(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/85 backdrop-blur-md animate-fadeIn">
<div className="relative w-full max-w-2xl bg-slate-900 border-2 border-cyan-500/50 rounded-2xl shadow-[0_0_50px_rgba(6,182,212,0.35)] overflow-hidden text-slate-100 flex flex-col max-h-[92vh]">
{/* Modal Header */}
<div className="p-4 bg-gradient-to-r from-slate-950 via-cyan-950/60 to-slate-950 border-b border-cyan-500/30 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-cyan-400 to-blue-600 rounded-xl flex items-center justify-center font-black text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.6)]">
<Lock className="w-5 h-5 text-slate-950" />
</div>
<div>
<h2 className="text-base font-bold text-white tracking-tight flex items-center gap-2">
CREDENTIAL AUTHENTICATION MATRIX
<span className="text-[10px] bg-cyan-500/20 text-cyan-300 px-2 py-0.5 rounded border border-cyan-400/40 font-mono">
MANDATORY PASSWORD
</span>
</h2>
<p className="text-xs text-slate-400 font-mono">
Designated Branch & Head Office Credentials Required for Session Entry
</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Feedback Banner */}
{loginError && !isForgotPassMode && (
<div className="mx-4 mt-3 p-3 bg-rose-950/80 border border-rose-500/60 rounded-xl text-rose-200 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
<span>{loginError}</span>
</div>
)}
{loginSuccess && !isForgotPassMode && (
<div className="mx-4 mt-3 p-3 bg-emerald-950/80 border border-emerald-500/60 rounded-xl text-emerald-200 text-xs flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
<span>{loginSuccess}</span>
</div>
)}
{/* Modal Body */}
<div className="p-4 overflow-y-auto space-y-4 flex-1">
{mustChangeMode ? (
/* Forced Password Reset / Set New Password View */
<div className="bg-slate-950 border border-amber-500/50 rounded-xl p-4 space-y-4 shadow-[0_0_20px_rgba(245,158,11,0.15)]">
<div className="flex items-center gap-3 border-b border-amber-500/20 pb-3">
<div className="p-2.5 bg-amber-500/20 border border-amber-500/40 rounded-lg text-amber-400">
<Key className="w-5 h-5" />
</div>
<div>
<h3 className="text-sm font-bold text-amber-300">Mandatory Password Update Required</h3>
<p className="text-xs text-slate-400">
Account: <span className="font-mono text-white">{pendingUser?.email}</span> ({pendingUser?.name})
</p>
</div>
</div>
{changePassError && (
<div className="p-2.5 bg-rose-950/80 border border-rose-500/60 rounded-lg text-rose-200 text-xs flex items-center gap-2">
<X className="w-4 h-4 text-rose-400 shrink-0" />
<span>{changePassError}</span>
</div>
)}
<form onSubmit={handleSetNewPassword} className="space-y-3">
<div className="space-y-1">
<label className="text-xs font-semibold text-slate-300">Enter New Password</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="At least 4 characters..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-400 font-mono"
required
/>
</div>
<div className="space-y-1">
<label className="text-xs font-semibold text-slate-300">Confirm New Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Re-enter new password..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-amber-400 font-mono"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-amber-500 via-orange-500 to-amber-600 text-slate-950 font-bold text-xs hover:from-amber-400 hover:to-orange-400 transition-all cursor-pointer flex items-center justify-center gap-2 shadow-[0_0_15px_rgba(245,158,11,0.3)]"
>
{isLoading ? (
<RefreshCw className="w-4 h-4 animate-spin text-slate-950" />
) : (
<ArrowRight className="w-4 h-4 text-slate-950" />
)}
<span>Set New Password & Authenticate Session</span>
</button>
</form>
</div>
) : isForgotPassMode ? (
/* Forgot Password Mechanism View */
<div className="bg-slate-950 border border-amber-500/40 rounded-xl p-4 space-y-4 shadow-[0_0_20px_rgba(245,158,11,0.1)]">
<div className="flex items-center justify-between border-b border-amber-500/20 pb-3">
<div className="flex items-center gap-2.5">
<div className="p-2 bg-amber-500/20 border border-amber-500/40 rounded-lg text-amber-400">
<RotateCcw className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-amber-300 uppercase tracking-wide font-mono">
Password Reset Recovery
</h3>
<p className="text-xs text-slate-400 font-mono">
Issue temporary credentials to registered officer email
</p>
</div>
</div>
<button
onClick={() => {
setIsForgotPassMode(false);
setForgotError(null);
setForgotStatus(null);
}}
className="text-xs text-slate-400 hover:text-white underline font-mono cursor-pointer"
>
Back to Login
</button>
</div>
{forgotError && (
<div className="p-3 bg-rose-950/80 border border-rose-500/60 rounded-xl text-rose-200 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
<span>{forgotError}</span>
</div>
)}
{forgotStatus ? (
<div className="p-4 bg-emerald-950/80 border border-emerald-500/60 rounded-xl text-emerald-200 space-y-3">
<div className="flex items-center gap-2 text-emerald-300 font-bold text-xs">
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
<span>Password Reset Dispatched</span>
</div>
<div className="bg-slate-950 border border-emerald-500/30 p-3 rounded-lg text-xs space-y-2 font-mono">
<div className="text-slate-200 leading-relaxed">
{forgotStatus.message}
</div>
<div className="text-[11px] text-emerald-400/90 flex items-center gap-1.5 pt-1 border-t border-slate-800">
<Mail className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
<span>Check the inbox or system email dispatch logs for {forgotStatus.userEmail}.</span>
</div>
</div>
<div className="pt-2 flex items-center gap-2">
<button
type="button"
onClick={() => {
if (forgotStatus.userEmail) setEmail(forgotStatus.userEmail);
setPassword('');
setIsForgotPassMode(false);
setForgotStatus(null);
}}
className="px-4 py-2 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold text-xs rounded-lg transition-colors cursor-pointer flex items-center gap-1.5"
>
<LogIn className="w-3.5 h-3.5" />
<span>Return to Login with Mailed Credential</span>
</button>
</div>
</div>
) : (
<form onSubmit={handleForgotPassword} className="space-y-3.5">
<div className="space-y-1.5">
<label className="text-xs text-slate-300 font-semibold flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Mail className="w-3.5 h-3.5 text-amber-400" />
Registered Officer Email Address
</span>
<span className="text-[10px] text-slate-500 font-mono">e.g. branch@networkbank.com</span>
</label>
<input
type="email"
value={forgotEmail}
onChange={(e) => setForgotEmail(e.target.value)}
placeholder="Enter registered email address..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-amber-400 font-mono"
required
/>
</div>
<div className="pt-2 flex items-center gap-2">
<button
type="submit"
disabled={isLoading}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-amber-500 via-orange-500 to-amber-600 text-slate-950 font-bold text-xs hover:from-amber-400 hover:to-orange-400 transition-all shadow-[0_0_15px_rgba(245,158,11,0.3)] cursor-pointer flex items-center justify-center gap-2"
>
{isLoading ? (
<RefreshCw className="w-4 h-4 animate-spin text-slate-950" />
) : (
<Send className="w-4 h-4 text-slate-950" />
)}
<span>Request Password Reset & Temporary Pass</span>
</button>
</div>
</form>
)}
</div>
) : (
/* Standard Credential Authentication Form */
<div className="space-y-4">
<form onSubmit={handleCredentialLogin} className="space-y-3.5 bg-slate-950/60 p-4 rounded-xl border border-slate-800">
<div className="space-y-1.5">
<label className="text-xs text-slate-300 font-semibold flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Mail className="w-3.5 h-3.5 text-cyan-400" />
Designated Officer Email / Username
</span>
<span className="text-[10px] text-slate-500 font-mono">e.g. branch@networkbank.com</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-cyan-400 font-mono"
required
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-slate-300 font-semibold flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Key className="w-3.5 h-3.5 text-amber-400" />
Password / Security Pin
</span>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
setIsForgotPassMode(true);
setForgotEmail(email);
}}
className="text-[10px] text-amber-400 hover:underline flex items-center gap-1 font-mono cursor-pointer"
>
<HelpCircle className="w-3 h-3 text-amber-400" />
<span>Forgot Password?</span>
</button>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-[10px] text-cyan-400 hover:underline flex items-center gap-1 cursor-pointer"
>
{showPassword ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
<span>{showPassword ? 'Hide' : 'Show'}</span>
</button>
</div>
</label>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-cyan-400 font-mono"
required
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-slate-300 font-semibold flex items-center gap-1.5">
<Building2 className="w-3.5 h-3.5 text-emerald-400" />
Branch Jurisdiction Scope
</label>
<select
value={selectedBranchId}
onChange={(e) => {
const val = e.target.value as BranchId | 'head_office';
setSelectedBranchId(val);
if (val === 'head_office' && (!email || email.includes('branch'))) {
setEmail('admin@networkbank.com');
}
}}
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-cyan-400 font-mono cursor-pointer"
>
<option value="head_office">Head Office Admin (HO)</option>
{BRANCHES_LIST.map((b) => (
<option key={b.id} value={b.id}>
{b.name} ({b.code})
</option>
))}
</select>
</div>
<div className="pt-2">
<button
type="submit"
disabled={isLoading}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-cyan-500 via-blue-600 to-indigo-600 text-slate-950 font-bold text-xs hover:from-cyan-400 hover:to-indigo-500 transition-all shadow-[0_0_20px_rgba(6,182,212,0.4)] cursor-pointer flex items-center justify-center gap-2"
>
{isLoading ? (
<RefreshCw className="w-4 h-4 animate-spin text-slate-950" />
) : (
<LogIn className="w-4 h-4 text-slate-950" />
)}
<span>Authenticate Session Credentials</span>
</button>
</div>
</form>
</div>
)}
</div>
{/* Modal Footer */}
<div className="p-3 bg-slate-950 border-t border-slate-800 text-[10px] text-slate-400 font-mono flex items-center justify-between shrink-0">
<span className="flex items-center gap-1 text-cyan-400">
<Shield className="w-3 h-3 text-cyan-400" />
Mandatory Credential Authentication Active
</span>
<span>Network Banking Security Engine</span>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,232 @@
import React, { useState, useEffect } from 'react';
import {
CheckCircle2,
AlertTriangle,
RefreshCw,
ShieldCheck,
Search,
Building2,
Zap,
ArrowRightLeft,
ChevronRight
} from 'lucide-react';
import { AutoCheckResult, BranchId, BRANCHES_LIST } from '../types';
interface AutoChecksViewProps {
period: string;
}
export const AutoChecksView: React.FC<AutoChecksViewProps> = ({ period }) => {
const [checks, setChecks] = useState<AutoCheckResult[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [selectedCheck, setSelectedCheck] = useState<AutoCheckResult | null>(null);
const fetchChecks = async () => {
setLoading(true);
try {
const res = await fetch(`/api/autochecks/${period}`);
const data = await res.json();
if (data.checks) {
setChecks(data.checks);
setSelectedCheck(data.checks[0] || null);
}
} catch (err) {
console.error('Error fetching auto-checks:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchChecks();
}, [period]);
const passCount = checks.filter((c) => c.status === 'pass').length;
const failCount = checks.filter((c) => c.status === 'fail').length;
const warnCount = checks.filter((c) => c.status === 'warning').length;
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Header Banner */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-3">
<div className="p-3 bg-emerald-600/20 border border-emerald-500/30 rounded-xl text-emerald-400">
<Zap className="w-6 h-6" />
</div>
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
Automated Inter-Network Reconciliation Checks
<span className="text-xs bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-2 py-0.5 rounded font-mono">
{period}
</span>
</h2>
<p className="text-xs text-slate-400">
Auto-validates network placements vs borrowings, HO funds, and balance sheet integrity with minimal human intervention.
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 text-xs font-mono bg-slate-950 p-2 rounded-lg border border-slate-800">
<span className="text-emerald-400 font-bold">{passCount} Passed</span>
<span className="text-slate-600">|</span>
<span className="text-rose-400 font-bold">{failCount} Mismatches</span>
<span className="text-slate-600">|</span>
<span className="text-amber-400 font-bold">{warnCount} Warnings</span>
</div>
<button
onClick={fetchChecks}
disabled={loading}
className="px-3.5 py-2 bg-blue-600 hover:bg-blue-500 text-white font-bold rounded-lg text-xs flex items-center space-x-1.5 shadow transition-all cursor-pointer"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
<span>Re-Run Auto Checks</span>
</button>
</div>
</div>
{/* Main Checks Grid & Diagnostic Panel */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Checks List */}
<div className="lg:col-span-1 space-y-3">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider px-1">
Reconciliation Modules ({checks.length})
</h3>
<div className="space-y-2">
{checks.map((check) => {
const isSelected = selectedCheck?.id === check.id;
return (
<div
key={check.id}
onClick={() => setSelectedCheck(check)}
className={`p-4 rounded-xl border transition-all cursor-pointer text-xs space-y-2 ${
isSelected
? 'bg-slate-800/90 border-blue-500/80 shadow-lg'
: 'bg-slate-900/80 border-slate-800 hover:bg-slate-800/50 text-slate-300'
}`}
>
<div className="flex items-start justify-between gap-2">
<span className="font-bold text-slate-100 flex items-center gap-1.5">
{check.status === 'pass' && <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />}
{check.status === 'fail' && <AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />}
{check.status === 'warning' && <AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" />}
{check.title}
</span>
<ChevronRight className={`w-4 h-4 text-slate-500 transition-transform ${isSelected ? 'translate-x-1 text-blue-400' : ''}`} />
</div>
<p className="text-[11px] text-slate-400 line-clamp-2 leading-relaxed">
{check.description}
</p>
<div className="flex justify-between items-center text-[10px] font-mono pt-1 border-t border-slate-800">
<span className="text-slate-500">Variance:</span>
<span className={`font-bold ${Math.abs(check.difference) > 0.01 ? 'text-rose-400' : 'text-emerald-400'}`}>
{check.difference === 0 ? 'USD 0.00 Mn' : `USD ${check.difference.toFixed(2)} Mn`}
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Selected Check Diagnostic Detail */}
{selectedCheck && (
<div className="lg:col-span-2 space-y-4">
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-5">
<div className="flex justify-between items-start border-b border-slate-800 pb-3">
<div>
<span className="text-[10px] uppercase font-mono tracking-wider text-blue-400 block mb-1">
{selectedCheck.category}
</span>
<h3 className="text-base font-bold text-white flex items-center gap-2">
{selectedCheck.title}
</h3>
</div>
<span
className={`text-xs font-bold px-3 py-1 rounded-full border ${
selectedCheck.status === 'pass'
? 'bg-emerald-950 text-emerald-300 border-emerald-500/40'
: selectedCheck.status === 'fail'
? 'bg-rose-950 text-rose-300 border-rose-500/40'
: 'bg-amber-950 text-amber-300 border-amber-500/40'
}`}
>
{selectedCheck.status.toUpperCase()}
</span>
</div>
{/* Metric Breakdown Cards */}
<div className="grid grid-cols-3 gap-3 font-mono">
<div className="bg-slate-950 p-3 rounded-lg border border-slate-800">
<span className="text-[10px] text-slate-400 block">Expected Ledger Total</span>
<span className="text-sm font-bold text-blue-300">USD {selectedCheck.expectedValue.toFixed(2)} Mn</span>
</div>
<div className="bg-slate-950 p-3 rounded-lg border border-slate-800">
<span className="text-[10px] text-slate-400 block">Reported Counterpart Total</span>
<span className="text-sm font-bold text-slate-200">USD {selectedCheck.actualValue.toFixed(2)} Mn</span>
</div>
<div className="bg-slate-950 p-3 rounded-lg border border-slate-800">
<span className="text-[10px] text-slate-400 block">Difference / Delta</span>
<span className={`text-sm font-bold ${Math.abs(selectedCheck.difference) > 0.01 ? 'text-rose-400' : 'text-emerald-400'}`}>
USD {selectedCheck.difference.toFixed(2)} Mn
</span>
</div>
</div>
{/* Detailed Diagnostic Analysis */}
<div className="bg-slate-950 p-4 rounded-xl border border-slate-800 space-y-2">
<h4 className="text-xs font-bold text-slate-300 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-blue-400" />
Automated Inspection Details
</h4>
<p className="text-xs text-slate-300 leading-relaxed">
{selectedCheck.details}
</p>
</div>
{/* Flagged Branches Breakdown */}
{selectedCheck.flaggedBranches.length > 0 && (
<div className="bg-rose-950/30 border border-rose-500/40 rounded-xl p-4 space-y-2">
<h4 className="text-xs font-bold text-rose-300 flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-rose-400" />
Flagged Branches Requiring Attention
</h4>
<div className="flex flex-wrap gap-2 pt-1">
{selectedCheck.flaggedBranches.map((bId) => {
const b = BRANCHES_LIST.find((br) => br.id === bId);
return (
<div
key={bId}
className="bg-rose-900/40 border border-rose-500/50 text-rose-200 px-3 py-1 rounded-lg text-xs font-semibold flex items-center space-x-1.5"
>
<Building2 className="w-3.5 h-3.5 text-rose-300" />
<span>{b?.name || bId}</span>
</div>
);
})}
</div>
</div>
)}
{/* Inter-Network Rules Reference Card */}
<div className="bg-slate-950/60 p-4 rounded-xl border border-slate-800 text-xs text-slate-400 space-y-1">
<div className="font-bold text-slate-200">System Policy Rule:</div>
<p className="leading-relaxed">
For inter-branch liquidity, <strong>Consolidated Placement - Network</strong> MUST mathematically equal <strong>Consolidated Borrowing - Network</strong> across the entire international entity web. Any asymmetry indicates an unreconciled inter-branch transaction.
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
};

View file

@ -0,0 +1,928 @@
import React, { useState, useEffect } from 'react';
import {
Building2,
CheckCircle2,
AlertTriangle,
Send,
HelpCircle,
TrendingUp,
TrendingDown,
Info,
ShieldAlert,
Sparkles,
RefreshCw,
Plus,
Trash2,
ArrowLeftRight,
Scale
} from 'lucide-react';
import {
BranchId,
BRANCHES_LIST,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
BalanceSheetItems,
BranchSubmission,
User,
VarianceComment,
CompositionItem,
CompositionBreakdown
} from '../types';
import { DEFAULT_SEED_COMPOSITIONS } from '../data/seedData';
interface BranchPortalViewProps {
currentUser: User;
period: string;
priorPeriod: string;
submissions: Record<BranchId, BranchSubmission>;
priorSubmissions: Record<BranchId, BranchSubmission>;
onSubmitSuccess: () => void;
}
// Composition Breakdown Manager Component for Other Assets & Other Liabilities
const CompositionEditor: React.FC<{
title: string;
type: 'otherAssets' | 'otherLiabilities';
items: CompositionItem[];
targetAmount: number;
onChange: (items: CompositionItem[]) => void;
onAutoSync: (sum: number) => void;
}> = ({ title, type, items, targetAmount, onChange, onAutoSync }) => {
const sum = items.reduce((acc, curr) => acc + (curr.amount || 0), 0);
const diff = Number((sum - targetAmount).toFixed(2));
const isMatched = Math.abs(diff) < 0.01;
const handleAddItem = () => {
const newItem: CompositionItem = {
id: `comp-${type}-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
description: '',
amount: 0,
};
onChange([...items, newItem]);
};
const handleUpdateItem = (id: string, field: 'description' | 'amount', value: any) => {
const updated = items.map((item) => {
if (item.id === id) {
return {
...item,
[field]: field === 'amount' ? (parseFloat(value) || 0) : value,
};
}
return item;
});
onChange(updated);
};
const handleDeleteItem = (id: string) => {
onChange(items.filter((item) => item.id !== id));
};
return (
<div className="bg-slate-950/95 border-2 border-indigo-500/60 rounded-xl p-3 text-xs space-y-3 shadow-[0_4px_20px_rgba(99,102,241,0.2)] my-3">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-indigo-500/40 pb-2">
<div className="flex items-center gap-2 font-bold text-indigo-300">
<Sparkles className="w-4 h-4 text-cyan-400 animate-pulse" />
<span className="text-sm font-semibold">{title} Composition Details</span>
<span className="text-[10px] text-indigo-300/80 font-mono bg-indigo-950/80 px-2 py-0.5 rounded border border-indigo-500/40">
{items.length} Component{items.length !== 1 ? 's' : ''}
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onAutoSync(sum)}
className="px-2.5 py-1 rounded bg-indigo-600/40 hover:bg-indigo-600/70 border border-indigo-400/70 text-indigo-100 text-[10px] font-mono font-bold transition-all flex items-center gap-1 cursor-pointer shadow-sm hover:scale-[1.02]"
title="Auto-update line item input to match sum of compositions"
>
<RefreshCw className="w-3 h-3 text-cyan-300" />
Auto-Sync Value (${sum.toFixed(1)}M)
</button>
<span
className={`text-[10px] font-mono font-bold px-2 py-0.5 rounded border ${
isMatched
? 'bg-emerald-950 text-emerald-300 border-emerald-500/60'
: 'bg-amber-950 text-amber-300 border-amber-500/60 animate-pulse'
}`}
>
Sum: ${sum.toFixed(1)}M {isMatched ? '✓ Matched' : `(Diff: ${diff > 0 ? '+' : ''}${diff.toFixed(1)}M)`}
</span>
</div>
</div>
{items.length === 0 ? (
<div className="text-[11px] text-slate-400 italic text-center py-2 bg-slate-900/60 rounded-lg border border-slate-800">
No breakdown items added yet. Click "+ Add Component" below to specify individual components.
</div>
) : (
<div className="space-y-2">
{items.map((item, idx) => (
<div key={item.id} className="flex items-center gap-2 bg-slate-900/90 p-2 rounded-lg border border-slate-700 hover:border-indigo-400/50 transition-colors">
<span className="text-[10px] font-mono text-indigo-400 font-bold w-4 text-center">{idx + 1}.</span>
<input
type="text"
value={item.description}
onChange={(e) => handleUpdateItem(item.id, 'description', e.target.value)}
placeholder="Component description (e.g., Accrued Fee Receivables, Prepaid Tax)"
className="flex-1 bg-slate-950 text-white text-xs px-2.5 py-1.5 rounded border border-slate-700 focus:outline-none focus:border-cyan-400 font-sans"
/>
<div className="flex items-center gap-1 bg-slate-950 px-2 py-1 rounded border border-slate-700">
<span className="text-cyan-400 font-mono text-[10px]">$</span>
<input
type="number"
step="0.1"
value={item.amount || ''}
onChange={(e) => handleUpdateItem(item.id, 'amount', e.target.value)}
placeholder="0.0"
className="w-20 bg-transparent text-cyan-300 text-xs font-mono font-bold text-right focus:outline-none"
/>
<span className="text-slate-400 font-mono text-[10px]">Mn</span>
</div>
<button
type="button"
onClick={() => handleDeleteItem(item.id)}
className="p-1.5 text-rose-400 hover:text-rose-200 hover:bg-rose-950/70 rounded transition-colors text-xs"
title="Remove component"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
<button
type="button"
onClick={handleAddItem}
className="w-full py-1.5 border border-dashed border-indigo-400/60 hover:border-indigo-300 rounded-lg text-indigo-200 text-[11px] font-medium hover:bg-indigo-950/50 transition-all text-center flex items-center justify-center gap-1.5 cursor-pointer"
>
<Plus className="w-3.5 h-3.5 text-cyan-400" />
<span>Add Component to {title} Composition</span>
</button>
</div>
);
};
export const BranchPortalView: React.FC<BranchPortalViewProps> = ({
currentUser,
period,
priorPeriod,
submissions,
priorSubmissions,
onSubmitSuccess,
}) => {
const isAdmin = currentUser.role === 'admin';
// Selected branch: if branch user, fixed to their branchId; if admin, allow switching branch
const [selectedBranchId, setSelectedBranchId] = useState<BranchId>(
currentUser.branchId || 'bahrain'
);
const selectedBranch = BRANCHES_LIST.find((b) => b.id === selectedBranchId) || BRANCHES_LIST[0];
// Current submission or draft for this branch
const existingSubmission = submissions[selectedBranchId];
const priorSubmission = priorSubmissions[selectedBranchId];
// Local Form state for Balance Sheet Items
const [items, setItems] = useState<BalanceSheetItems>({
balancesWithBanks: 0,
balanceCentralBank: 0,
placementHO: 0,
placementNetwork: 0,
placementOutsideNetwork: 0,
tradeAsset: 0,
netAdvances: 0,
investments: 0,
otherAssets: 0,
fiDepositBanksOnly: 0,
customerDeposit: 0,
depositsNbpNetwork: 0,
borrowingNetwork: 0,
borrowingOutsideNetwork: 0,
borrowingHO: 0,
headOfficeSupportFund: 0,
otherLiabilities: 0,
fullPledged: 0,
partialPledged: 0,
});
// Local state for compositions
const [compositions, setCompositions] = useState<CompositionBreakdown>({
otherAssets: [],
otherLiabilities: [],
});
// Local state for variance comments
const [commentsMap, setCommentsMap] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [submitSuccessMsg, setSubmitSuccessMsg] = useState<string | null>(null);
const [showAuditModal, setShowAuditModal] = useState<boolean>(false);
// Auto-derived / carried schedule keys
const AUTO_CARRIED_KEYS: (keyof BalanceSheetItems)[] = [
'placementHO',
'placementNetwork',
'placementOutsideNetwork',
'borrowingHO',
'borrowingNetwork',
'borrowingOutsideNetwork',
'investments',
];
// Fetch and auto-import Placements, Borrowings, and Investments totals from schedule
const handleImportSchedules = async () => {
try {
const [plcRes, borRes, invRes] = await Promise.all([
fetch(`/api/placements?period=${encodeURIComponent(period)}&branchId=${selectedBranchId}`),
fetch(`/api/borrowings?period=${encodeURIComponent(period)}&branchId=${selectedBranchId}`),
fetch(`/api/investments?branchId=${selectedBranchId}`),
]);
if (plcRes.ok && borRes.ok) {
const plcData = await plcRes.json();
const borData = await borRes.json();
const invData = invRes.ok ? await invRes.json() : { investments: [] };
const plcList: any[] = plcData.placements || [];
const borList: any[] = borData.borrowings || [];
const invList: any[] = invData.investments || [];
const plcHO = plcList.filter((p) => p.type === 'Head Office').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0);
const plcNet = plcList.filter((p) => p.type === 'Inter Branch').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0);
const plcOut = plcList.filter((p) => p.type === 'Inter Bank').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0);
const borHO = borList.filter((b) => b.type === 'Head Office').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0);
const borNet = borList.filter((b) => b.type === 'Inter Branch').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0);
const borOut = borList.filter((b) => b.type === 'Inter Bank').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0);
const invTotal = invList.reduce((sum, inv) => sum + (inv.amountInvested || inv.faceValue || 0), 0);
setItems((prev) => ({
...prev,
placementHO: Number(plcHO.toFixed(2)),
placementNetwork: Number(plcNet.toFixed(2)),
placementOutsideNetwork: Number(plcOut.toFixed(2)),
borrowingHO: Number(borHO.toFixed(2)),
borrowingNetwork: Number(borNet.toFixed(2)),
borrowingOutsideNetwork: Number(borOut.toFixed(2)),
investments: Number(invTotal.toFixed(2)),
}));
setSubmitSuccessMsg(`Auto-synced Schedule Totals for ${selectedBranch.name}: Placements: $${(plcHO + plcNet + plcOut).toFixed(2)}Mn | Borrowings: $${(borHO + borNet + borOut).toFixed(2)}Mn | Investments: $${invTotal.toFixed(2)}Mn`);
}
} catch (err) {
console.error('Failed to import schedule totals:', err);
}
};
// Sync form state when selected branch or period changes
useEffect(() => {
if (existingSubmission) {
setItems({ ...existingSubmission.items });
if (existingSubmission.compositions) {
setCompositions(JSON.parse(JSON.stringify(existingSubmission.compositions)));
} else {
const seedComp = DEFAULT_SEED_COMPOSITIONS[selectedBranchId] || { otherAssets: [], otherLiabilities: [] };
setCompositions(JSON.parse(JSON.stringify(seedComp)));
}
const commentObj: Record<string, string> = {};
existingSubmission.varianceComments.forEach((vc) => {
commentObj[vc.lineItemKey] = vc.comment;
});
setCommentsMap(commentObj);
} else if (priorSubmission) {
setItems({ ...priorSubmission.items });
if (priorSubmission.compositions) {
setCompositions(JSON.parse(JSON.stringify(priorSubmission.compositions)));
} else {
const seedComp = DEFAULT_SEED_COMPOSITIONS[selectedBranchId] || { otherAssets: [], otherLiabilities: [] };
setCompositions(JSON.parse(JSON.stringify(seedComp)));
}
setCommentsMap({});
} else {
const seedComp = DEFAULT_SEED_COMPOSITIONS[selectedBranchId] || { otherAssets: [], otherLiabilities: [] };
setCompositions(JSON.parse(JSON.stringify(seedComp)));
}
// Auto-carry schedule totals from Placements, Borrowings & Investments tabs
handleImportSchedules();
setSubmitError(null);
setSubmitSuccessMsg(null);
}, [selectedBranchId, period, existingSubmission, priorSubmission]);
// Handle number input changes
const handleItemChange = (key: keyof BalanceSheetItems, valStr: string) => {
const val = parseFloat(valStr) || 0;
setItems((prev) => ({ ...prev, [key]: val }));
};
// Handle comment input changes
const handleCommentChange = (key: string, text: string) => {
setCommentsMap((prev) => ({ ...prev, [key]: text }));
};
// Calculations
const totalAssets = (
items.balancesWithBanks +
items.balanceCentralBank +
items.placementHO +
items.placementNetwork +
items.placementOutsideNetwork +
items.tradeAsset +
items.netAdvances +
items.investments +
items.otherAssets
);
const totalLiabilities = (
items.fiDepositBanksOnly +
items.customerDeposit +
items.depositsNbpNetwork +
items.borrowingNetwork +
items.borrowingOutsideNetwork +
items.borrowingHO +
items.headOfficeSupportFund +
items.otherLiabilities
);
const netAssets = totalAssets - totalLiabilities;
const totalLiabAndEquity = totalLiabilities + netAssets; // mathematically equal to Total Assets
const diffAmount = Number((totalAssets - totalLiabAndEquity).toFixed(2));
const isBalanced = Math.abs(diffAmount) < 0.01;
// Identify line items with excessive variance (|variance| >= 2.0 Mn)
const excessiveItems: {
key: keyof BalanceSheetItems;
label: string;
prevVal: number;
newVal: number;
variance: number;
}[] = [];
const checkItemVariance = (key: keyof BalanceSheetItems, label: string) => {
const prevVal = priorSubmission?.items[key] || 0;
const newVal = items[key] || 0;
const variance = newVal - prevVal;
if (Math.abs(variance) >= 2.0) {
excessiveItems.push({ key, label, prevVal, newVal, variance });
}
};
ASSET_ITEMS_CONFIG.forEach((cfg) => checkItemVariance(cfg.key, cfg.label));
LIABILITY_ITEMS_CONFIG.forEach((cfg) => checkItemVariance(cfg.key, cfg.label));
// Check how many excessive comments are filled
const missingCommentsCount = excessiveItems.filter(
(item) => !commentsMap[item.key] || commentsMap[item.key].trim().length < 5
).length;
// Pledge validation: Full pledge + Partial pledge cannot exceed total assets / pledge limit
const totalPledged = (items.fullPledged || 0) + (items.partialPledged || 0);
const isPledgeExceeded = totalPledged > totalAssets || (items.fullPledged || 0) > totalAssets || (items.partialPledged || 0) > totalAssets;
const canSubmit = isBalanced && missingCommentsCount === 0 && !isPledgeExceeded;
// Submit Handler
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError(null);
setSubmitSuccessMsg(null);
// Format variance comments array
const varianceCommentsList: VarianceComment[] = excessiveItems.map((item) => ({
lineItemKey: item.key,
lineItemLabel: item.label,
previousValue: item.prevVal,
newValue: item.newVal,
variance: item.variance,
comment: commentsMap[item.key] || '',
}));
try {
const res = await fetch('/api/balancesheets/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
period,
branchId: selectedBranchId,
userEmail: currentUser.email,
items,
compositions,
varianceComments: varianceCommentsList,
}),
});
const data = await res.json();
if (!res.ok) {
setSubmitError(data.error || 'Failed to submit balance sheet.');
} else {
setSubmitSuccessMsg(`Balance Sheet for ${selectedBranch.name} (${period}) submitted and reconciled successfully!`);
onSubmitSuccess();
}
} catch (err: any) {
setSubmitError(err.message || 'Server error submitting balance sheet');
} finally {
setIsSubmitting(false);
}
};
// Fetch and auto-import Placements and Borrowings totals from schedule
const handleImportPlacementsBorrowings = async () => {
try {
const [plcRes, borRes] = await Promise.all([
fetch(`/api/placements?period=${encodeURIComponent(period)}&branchId=${selectedBranchId}`),
fetch(`/api/borrowings?period=${encodeURIComponent(period)}&branchId=${selectedBranchId}`),
]);
if (plcRes.ok && borRes.ok) {
const plcData = await plcRes.json();
const borData = await borRes.json();
const plcList: any[] = plcData.placements || [];
const borList: any[] = borData.borrowings || [];
const plcNet = plcList.filter((p) => p.type === 'Inter Branch').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0);
const plcOut = plcList.filter((p) => p.type === 'Inter Bank').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0);
const borNet = borList.filter((b) => b.type === 'Inter Branch').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0);
const borOut = borList.filter((b) => b.type === 'Inter Bank').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0);
setItems((prev) => ({
...prev,
placementNetwork: Number(plcNet.toFixed(2)),
placementOutsideNetwork: Number(plcOut.toFixed(2)),
borrowingNetwork: Number(borNet.toFixed(2)),
borrowingOutsideNetwork: Number(borOut.toFixed(2)),
}));
setSubmitSuccessMsg(`Successfully imported Placements & Borrowings schedule totals for ${selectedBranch.name}! Placements: $${(plcNet + plcOut).toFixed(2)}Mn | Borrowings: $${(borNet + borOut).toFixed(2)}Mn`);
}
} catch (err) {
console.error('Failed to import placements & borrowings:', err);
}
};
return (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Branch Selection & Access Header */}
<div className="holo-card rounded-2xl p-5 shadow-[0_0_30px_rgba(6,182,212,0.1)] flex flex-wrap items-center justify-between gap-4 border border-cyan-500/30">
<div className="flex items-center space-x-3">
<div className="p-3 bg-cyan-500/20 border border-cyan-400/50 rounded-xl text-cyan-300 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
<Building2 className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-lg font-bold text-white glow-text-cyan">{selectedBranch.name}</h2>
<span className="text-xs bg-cyan-500/20 text-cyan-300 border border-cyan-500/40 px-2 py-0.5 rounded-full font-mono">
{selectedBranch.code}
</span>
{selectedBranch.isClosingDown && (
<span className="text-xs bg-rose-500/20 text-rose-300 border border-rose-500/40 px-2 py-0.5 rounded-full font-mono">
CLOSING DOWN BRANCH
</span>
)}
</div>
<p className="text-xs text-cyan-200/70 font-mono">
Quantum Entry Portal for Period Ending <strong className="text-white">{period}</strong> (Versus {priorPeriod})
</p>
</div>
</div>
{/* Admin Switcher or Branch Officer Info */}
<div className="flex items-center space-x-3">
<button
type="button"
onClick={handleImportSchedules}
className="px-3 py-1.5 rounded-xl bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 border border-emerald-500/50 text-xs font-mono font-bold flex items-center gap-1.5 shadow-sm transition-all cursor-pointer"
title="Auto-sync Placements, Borrowings & Investments line items from schedule tabs"
>
<ArrowLeftRight className="w-3.5 h-3.5" />
<span>Sync Schedule Totals</span>
</button>
{isAdmin ? (
<div className="flex items-center space-x-2 bg-slate-950/80 px-3 py-1.5 rounded-xl border border-cyan-500/30 shadow-inner">
<span className="text-xs text-cyan-300 font-mono">Switch Branch:</span>
<select
value={selectedBranchId}
onChange={(e) => setSelectedBranchId(e.target.value as BranchId)}
className="bg-slate-900 text-cyan-300 text-xs font-mono font-bold rounded-lg px-2.5 py-1 border border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-400 cursor-pointer"
>
{BRANCHES_LIST.map((b) => (
<option key={b.id} value={b.id}>
{b.name} ({b.code})
</option>
))}
</select>
</div>
) : (
<div className="text-right font-mono">
<span className="text-[10px] uppercase tracking-widest text-cyan-400/80 block">AUTHENTICATED OFFICER</span>
<span className="text-xs font-bold text-white glow-text-cyan">{currentUser.name}</span>
</div>
)}
</div>
</div>
{/* Real-time Balancing & Reconciliation Control Bar */}
<div
className={`p-4 rounded-2xl border flex flex-wrap items-center justify-between gap-4 transition-all shadow-lg ${
isBalanced
? 'bg-emerald-950/40 border-emerald-500/60 text-emerald-200 shadow-[0_0_20px_rgba(16,185,129,0.15)]'
: 'bg-rose-950/40 border-rose-500/60 text-rose-200 shadow-[0_0_20px_rgba(244,63,94,0.15)]'
}`}
>
<div className="flex items-center space-x-3">
{isBalanced ? (
<CheckCircle2 className="w-6 h-6 text-emerald-400 shrink-0 shadow-[0_0_10px_#10b981]" />
) : (
<AlertTriangle className="w-6 h-6 text-rose-400 shrink-0 animate-bounce" />
)}
<div>
<div className="text-sm font-bold flex items-center gap-2 font-mono">
{isBalanced ? 'BALANCE SHEET RECONCILED' : 'BALANCING DISCREPANCY DETECTED'}
<span className="text-xs font-mono px-2.5 py-0.5 rounded-full bg-slate-900 border border-slate-700">
DELTA: USD {diffAmount.toFixed(2)} Mn
</span>
</div>
<p className="text-xs text-slate-300">
{isBalanced
? 'Total Assets equal Total Liabilities & Equity ($' + totalAssets.toFixed(2) + ' Mn).'
: 'Total Assets ($' + totalAssets.toFixed(2) + ' Mn) does not match Total Liabilities & Equity ($' + totalLiabAndEquity.toFixed(2) + ' Mn). Adjust inputs.'}
</p>
</div>
</div>
{/* Quick Summary Numbers */}
<div className="flex items-center space-x-4 text-xs font-mono">
<div className="bg-slate-900/90 px-3.5 py-1.5 rounded-xl border border-cyan-500/30 text-center shadow-inner">
<span className="text-cyan-400/80 block text-[10px]">TOTAL ASSETS</span>
<span className="font-bold text-cyan-300">${totalAssets.toFixed(1)}M</span>
</div>
<div className="bg-slate-900/90 px-3.5 py-1.5 rounded-xl border border-cyan-500/30 text-center shadow-inner">
<span className="text-slate-400 block text-[10px]">TOTAL LIAB</span>
<span className="font-bold text-slate-200">${totalLiabilities.toFixed(1)}M</span>
</div>
<div className="bg-slate-900/90 px-3.5 py-1.5 rounded-xl border border-cyan-500/30 text-center shadow-inner">
<span className="text-emerald-400/80 block text-[10px]">NET ASSETS</span>
<span className={`font-bold ${netAssets < 0 ? 'text-rose-400' : 'text-emerald-300'}`}>
${netAssets.toFixed(1)}M
</span>
</div>
</div>
</div>
{/* Excessive Variance (> USD 2 Mn) Warning & Comment Requirements */}
{excessiveItems.length > 0 && (
<div className="bg-amber-950/40 border border-amber-500/50 rounded-xl p-4 text-amber-200 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 font-bold text-sm text-amber-300">
<ShieldAlert className="w-5 h-5 text-amber-400" />
<span>Excessive Variance Check (&gt; USD 2.0 Mn)</span>
</div>
<span className="text-xs font-mono bg-amber-900/60 border border-amber-700 px-2 py-0.5 rounded">
{excessiveItems.length - missingCommentsCount} / {excessiveItems.length} Reasons Provided
</span>
</div>
<p className="text-xs text-amber-200/90 leading-relaxed">
Policy requires an explanatory comment for any line item changing by <strong>USD 2.0 Mn or more</strong> from the prior submitted balance sheet ({priorPeriod}). Submission is blocked until all reasons are provided.
</p>
</div>
)}
{/* Submit Success / Error Messages */}
{submitError && (
<div className="bg-rose-950 border border-rose-500 text-rose-200 p-3 rounded-xl text-xs font-medium flex items-center space-x-2">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
<span>{submitError}</span>
</div>
)}
{submitSuccessMsg && (
<div className="bg-emerald-950 border border-emerald-500 text-emerald-200 p-3 rounded-xl text-xs font-medium flex items-center space-x-2">
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
<span>{submitSuccessMsg}</span>
</div>
)}
{/* Main Input Form */}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Assets Form Card */}
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg">
<div className="bg-slate-950 px-5 py-3 border-b border-slate-800 flex justify-between items-center">
<h3 className="text-sm font-bold text-blue-300 uppercase tracking-wider flex items-center gap-2">
1. Assets (USD Mn)
</h3>
<span className="text-xs font-mono text-slate-400">Total: <strong className="text-amber-300">${totalAssets.toFixed(2)} Mn</strong></span>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-12 gap-3 text-xs font-bold text-slate-400 border-b border-slate-800 pb-2 px-2">
<div className="col-span-5">Asset Line Item</div>
<div className="col-span-2 text-right">Prior ({priorPeriod})</div>
<div className="col-span-2 text-right">New Input ({period})</div>
<div className="col-span-3 text-right">Variance</div>
</div>
{ASSET_ITEMS_CONFIG.map((cfg) => {
const prevVal = priorSubmission?.items[cfg.key] || 0;
const newVal = items[cfg.key] || 0;
const variance = newVal - prevVal;
const isExcessive = Math.abs(variance) >= 2.0;
const isAutoCarried = AUTO_CARRIED_KEYS.includes(cfg.key);
return (
<div key={cfg.key} className="space-y-2 bg-slate-950/50 p-2.5 rounded-lg border border-slate-800/80">
<div className="grid grid-cols-12 gap-3 items-center text-xs">
<div className="col-span-5 font-semibold text-slate-200 flex items-center">
<span>{cfg.label}</span>
{isAutoCarried && (
<span className="text-[9px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-1.5 py-0.5 rounded font-mono ml-2 shrink-0">
Auto-Carried Schedule
</span>
)}
</div>
<div className="col-span-2 text-right font-mono text-slate-400">
{prevVal.toFixed(1)}
</div>
<div className="col-span-2 text-right">
<input
type="number"
step="0.01"
readOnly={isAutoCarried}
value={items[cfg.key]}
onChange={(e) => handleItemChange(cfg.key, e.target.value)}
className={`w-full text-right font-mono text-xs font-bold rounded px-2 py-1 focus:outline-none ${
isAutoCarried
? 'bg-emerald-950/60 text-emerald-300 border border-emerald-500/40 cursor-not-allowed shadow-inner'
: isExcessive
? 'bg-slate-900 border border-amber-500 text-amber-200 focus:ring-amber-400'
: 'bg-slate-900 border border-slate-700 text-white focus:ring-blue-500'
}`}
title={isAutoCarried ? 'Data is carried automatically from schedule module' : ''}
/>
</div>
<div className="col-span-3 text-right font-mono font-bold flex items-center justify-end space-x-1">
<span className={variance < 0 ? 'text-rose-400' : variance > 0 ? 'text-emerald-400' : 'text-slate-400'}>
{variance > 0 ? `+${variance.toFixed(1)}` : variance.toFixed(1)}
</span>
{isExcessive && (
<span className="text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-1 py-0.2 rounded font-sans">
&gt; $2M
</span>
)}
</div>
</div>
{/* Other Assets Composition breakdown editor */}
{cfg.key === 'otherAssets' && (
<CompositionEditor
title="Other Assets"
type="otherAssets"
items={compositions.otherAssets || []}
targetAmount={items.otherAssets}
onChange={(newCompItems) =>
setCompositions((prev) => ({ ...prev, otherAssets: newCompItems }))
}
onAutoSync={(sum) => setItems((prev) => ({ ...prev, otherAssets: sum }))}
/>
)}
{/* Inline Mandatory Comment Box for Excessive Variance */}
{isExcessive && (
<div className="pt-2 border-t border-amber-900/40">
<label className="text-[11px] font-semibold text-amber-300 flex items-center gap-1 mb-1">
<Info className="w-3.5 h-3.5" />
<span>Reason for change (&gt; USD 2 Mn variance) - Mandatory:</span>
</label>
<input
type="text"
placeholder="e.g. Placement of excess liquidity / matured treasury bonds..."
value={commentsMap[cfg.key] || ''}
onChange={(e) => handleCommentChange(cfg.key, e.target.value)}
className="w-full text-xs bg-slate-900 border border-amber-500/60 rounded px-3 py-1.5 text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
)}
</div>
);
})}
</div>
</div>
{/* Liabilities Form Card */}
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg">
<div className="bg-slate-950 px-5 py-3 border-b border-slate-800 flex justify-between items-center">
<h3 className="text-sm font-bold text-blue-300 uppercase tracking-wider flex items-center gap-2">
2. Liabilities (USD Mn)
</h3>
<span className="text-xs font-mono text-slate-400">Total: <strong className="text-amber-300">${totalLiabilities.toFixed(2)} Mn</strong></span>
</div>
<div className="p-4 space-y-3">
<div className="grid grid-cols-12 gap-3 text-xs font-bold text-slate-400 border-b border-slate-800 pb-2 px-2">
<div className="col-span-5">Liability Line Item</div>
<div className="col-span-2 text-right">Prior ({priorPeriod})</div>
<div className="col-span-2 text-right">New Input ({period})</div>
<div className="col-span-3 text-right">Variance</div>
</div>
{LIABILITY_ITEMS_CONFIG.map((cfg) => {
const prevVal = priorSubmission?.items[cfg.key] || 0;
const newVal = items[cfg.key] || 0;
const variance = newVal - prevVal;
const isExcessive = Math.abs(variance) >= 2.0;
const isAutoCarried = AUTO_CARRIED_KEYS.includes(cfg.key);
return (
<div key={cfg.key} className="space-y-2 bg-slate-950/50 p-2.5 rounded-lg border border-slate-800/80">
<div className="grid grid-cols-12 gap-3 items-center text-xs">
<div className="col-span-5 font-semibold text-slate-200 flex items-center">
<span>{cfg.label}</span>
{isAutoCarried && (
<span className="text-[9px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-1.5 py-0.5 rounded font-mono ml-2 shrink-0">
Auto-Carried Schedule
</span>
)}
</div>
<div className="col-span-2 text-right font-mono text-slate-400">
{prevVal.toFixed(1)}
</div>
<div className="col-span-2 text-right">
<input
type="number"
step="0.01"
readOnly={isAutoCarried}
value={items[cfg.key]}
onChange={(e) => handleItemChange(cfg.key, e.target.value)}
className={`w-full text-right font-mono text-xs font-bold rounded px-2 py-1 focus:outline-none ${
isAutoCarried
? 'bg-emerald-950/60 text-emerald-300 border border-emerald-500/40 cursor-not-allowed shadow-inner'
: isExcessive
? 'bg-slate-900 border border-amber-500 text-amber-200 focus:ring-amber-400'
: 'bg-slate-900 border border-slate-700 text-white focus:ring-blue-500'
}`}
title={isAutoCarried ? 'Data is carried automatically from schedule module' : ''}
/>
</div>
<div className="col-span-3 text-right font-mono font-bold flex items-center justify-end space-x-1">
<span className={variance < 0 ? 'text-rose-400' : variance > 0 ? 'text-emerald-400' : 'text-slate-400'}>
{variance > 0 ? `+${variance.toFixed(1)}` : variance.toFixed(1)}
</span>
{isExcessive && (
<span className="text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-1 py-0.2 rounded font-sans">
&gt; $2M
</span>
)}
</div>
</div>
{/* Other Liabilities Composition breakdown editor */}
{cfg.key === 'otherLiabilities' && (
<CompositionEditor
title="Other Liabilities"
type="otherLiabilities"
items={compositions.otherLiabilities || []}
targetAmount={items.otherLiabilities}
onChange={(newCompItems) =>
setCompositions((prev) => ({ ...prev, otherLiabilities: newCompItems }))
}
onAutoSync={(sum) => setItems((prev) => ({ ...prev, otherLiabilities: sum }))}
/>
)}
{/* Inline Mandatory Comment Box for Excessive Variance */}
{isExcessive && (
<div className="pt-2 border-t border-amber-900/40">
<label className="text-[11px] font-semibold text-amber-300 flex items-center gap-1 mb-1">
<Info className="w-3.5 h-3.5" />
<span>Reason for change (&gt; USD 2 Mn variance) - Mandatory:</span>
</label>
<input
type="text"
placeholder="e.g. External borrowing matured / increase in PNSC deposit..."
value={commentsMap[cfg.key] || ''}
onChange={(e) => handleCommentChange(cfg.key, e.target.value)}
className="w-full text-xs bg-slate-900 border border-amber-500/60 rounded px-3 py-1.5 text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
)}
</div>
);
})}
</div>
</div>
{/* 3. Mandatory Requirements & Pledging */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="text-sm font-bold text-blue-300 uppercase tracking-wider">
3. Mandatory Requirements (SLR, CRR and other mandatory requirements)
</h3>
<div className={`text-xs font-mono font-bold px-3 py-1 rounded-full border ${
isPledgeExceeded
? 'bg-rose-950/80 text-rose-300 border-rose-500/80 animate-pulse'
: 'bg-slate-950 text-cyan-300 border-cyan-500/40'
}`}>
Total Pledged: ${totalPledged.toFixed(1)}M / Max Limit: ${totalAssets.toFixed(1)}M Total Assets
</div>
</div>
{isPledgeExceeded && (
<div className="bg-rose-950/70 border border-rose-500/80 rounded-lg p-3 text-rose-200 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
<span>
<strong>Pledging Limit Exceeded:</strong> Full Pledged (${(items.fullPledged || 0).toFixed(1)}M) + Partial Pledged (${(items.partialPledged || 0).toFixed(1)}M) = <strong>${totalPledged.toFixed(1)}M</strong>, which cannot exceed Total Assets (${totalAssets.toFixed(1)}M).
</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={`bg-pink-950/30 border rounded-lg p-3 space-y-2 ${isPledgeExceeded ? 'border-rose-500' : 'border-pink-500/40'}`}>
<label className="text-xs font-bold text-pink-300 flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-pink-500 inline-block"></span>
Full Pledged Assets (USD Mn)
</label>
<input
type="number"
step="0.01"
value={items.fullPledged}
onChange={(e) => handleItemChange('fullPledged', e.target.value)}
className="w-full text-right font-mono text-xs font-bold rounded px-3 py-1.5 bg-slate-950 border border-pink-500/50 text-white focus:outline-none focus:ring-1 focus:ring-pink-400"
/>
</div>
<div className={`bg-yellow-950/30 border rounded-lg p-3 space-y-2 ${isPledgeExceeded ? 'border-rose-500' : 'border-yellow-500/40'}`}>
<label className="text-xs font-bold text-yellow-300 flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-yellow-400 inline-block"></span>
Partial Pledged Assets (USD Mn)
</label>
<input
type="number"
step="0.01"
value={items.partialPledged}
onChange={(e) => handleItemChange('partialPledged', e.target.value)}
className="w-full text-right font-mono text-xs font-bold rounded px-3 py-1.5 bg-slate-950 border border-yellow-500/50 text-white focus:outline-none focus:ring-1 focus:ring-yellow-400"
/>
</div>
</div>
</div>
{/* Bottom Submission & Verification Bar */}
<div className="bg-slate-950 border border-slate-800 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4 sticky bottom-4 shadow-2xl">
<div className="text-xs text-slate-300 space-y-1">
<div className="font-bold text-white flex items-center gap-2">
Submission Readiness Checklist:
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
<span className={`flex items-center gap-1 ${isBalanced ? 'text-emerald-400' : 'text-rose-400'}`}>
{isBalanced ? <CheckCircle2 className="w-3.5 h-3.5" /> : <AlertTriangle className="w-3.5 h-3.5" />}
Assets = Liab & Equity (Diff $0.00M)
</span>
<span className={`flex items-center gap-1 ${missingCommentsCount === 0 ? 'text-emerald-400' : 'text-amber-400'}`}>
{missingCommentsCount === 0 ? <CheckCircle2 className="w-3.5 h-3.5" /> : <AlertTriangle className="w-3.5 h-3.5" />}
Excessive Variance Explanations ({missingCommentsCount} missing)
</span>
<span className={`flex items-center gap-1 ${!isPledgeExceeded ? 'text-emerald-400' : 'text-rose-400'}`}>
{!isPledgeExceeded ? <CheckCircle2 className="w-3.5 h-3.5" /> : <AlertTriangle className="w-3.5 h-3.5" />}
Pledged Assets ({totalPledged.toFixed(1)}M &le; {totalAssets.toFixed(1)}M Limit)
</span>
</div>
</div>
<button
type="submit"
disabled={!canSubmit || isSubmitting}
className={`px-6 py-3 rounded-xl font-bold text-xs flex items-center space-x-2 shadow-lg transition-all ${
canSubmit && !isSubmitting
? 'bg-blue-600 hover:bg-blue-500 text-white shadow-blue-900/50 cursor-pointer'
: 'bg-slate-800 text-slate-500 cursor-not-allowed border border-slate-700'
}`}
>
{isSubmitting ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4" />
)}
<span>Submit Balance Sheet to Head Office</span>
</button>
</div>
</form>
</div>
);
};

View file

@ -0,0 +1,749 @@
import React, { useState } from 'react';
import {
Download,
Filter,
MessageSquare,
AlertTriangle,
FileSpreadsheet,
CheckCircle2,
HelpCircle,
Eye,
TrendingUp,
TrendingDown,
Scale
} from 'lucide-react';
import {
BranchId,
BRANCHES_LIST,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
BranchSubmission,
BalanceSheetItems,
VarianceComment,
User
} from '../types';
interface ConsolidatedViewProps {
period: string;
priorPeriod: string;
submissions: Record<BranchId, BranchSubmission>;
priorSubmissions: Record<BranchId, BranchSubmission>;
currentUser?: User;
}
export const ConsolidatedView: React.FC<ConsolidatedViewProps> = ({
period,
priorPeriod,
submissions,
priorSubmissions,
currentUser,
}) => {
const [viewMode, setViewMode] = useState<'current' | 'prior' | 'variance'>('current');
const [showComments, setShowComments] = useState<boolean>(true);
const [highlightPledged, setHighlightPledged] = useState<boolean>(true);
const [highlightExcessive, setHighlightExcessive] = useState<boolean>(true);
const [showAuditModal, setShowAuditModal] = useState<boolean>(false);
const activeBranchList = BRANCHES_LIST.filter((b) => !b.isClosingDown);
const closingBranchList = BRANCHES_LIST.filter((b) => b.isClosingDown);
// Helper to get raw value for branch & item key in current or prior period
const getValue = (bId: BranchId, key: keyof BalanceSheetItems, isPrior = false): number => {
const map = isPrior ? priorSubmissions : submissions;
return map[bId]?.items[key] || 0;
};
// Helper for line total across all 10 branches
const getLineTotal = (key: keyof BalanceSheetItems, isPrior = false): number => {
return BRANCHES_LIST.reduce((acc, b) => acc + getValue(b.id, key, isPrior), 0);
};
// Helper for variance value
const getVariance = (bId: BranchId, key: keyof BalanceSheetItems): number => {
const curr = getValue(bId, key, false);
const prev = getValue(bId, key, true);
return curr - prev;
};
const getVarianceTotal = (key: keyof BalanceSheetItems): number => {
return getLineTotal(key, false) - getLineTotal(key, true);
};
// Compute Total Assets for branch
const getBranchTotalAssets = (bId: BranchId, isPrior = false): number => {
return ASSET_ITEMS_CONFIG.reduce((sum, cfg) => sum + getValue(bId, cfg.key, isPrior), 0);
};
// Compute Total Liabilities for branch
const getBranchTotalLiabilities = (bId: BranchId, isPrior = false): number => {
return LIABILITY_ITEMS_CONFIG.reduce((sum, cfg) => sum + getValue(bId, cfg.key, isPrior), 0);
};
// Compute Net Assets for branch
const getBranchNetAssets = (bId: BranchId, isPrior = false): number => {
return getBranchTotalAssets(bId, isPrior) - getBranchTotalLiabilities(bId, isPrior);
};
// Aggregate all variance comments for active period across all branches
const allComments: { branchId: BranchId; comment: VarianceComment }[] = [];
BRANCHES_LIST.forEach((b) => {
const sub = submissions[b.id];
if (sub && sub.varianceComments) {
sub.varianceComments.forEach((vc) => {
allComments.push({ branchId: b.id, comment: vc });
});
}
});
// Export to CSV
const exportToCSV = () => {
let csvContent = `International Network Balance Sheet (USD Mn) - Period: ${period}\n`;
csvContent += `Line Item,Total,${BRANCHES_LIST.map((b) => b.name).join(',')}\n`;
// Assets
csvContent += `ASSETS\n`;
ASSET_ITEMS_CONFIG.forEach((cfg) => {
const lineTot = getLineTotal(cfg.key);
const vals = BRANCHES_LIST.map((b) => getValue(b.id, cfg.key)).join(',');
csvContent += `"${cfg.label}",${lineTot.toFixed(1)},${vals}\n`;
});
const totAssets = BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalAssets(b.id), 0);
csvContent += `"Total Assets",${totAssets.toFixed(1)},${BRANCHES_LIST.map((b) => getBranchTotalAssets(b.id).toFixed(1)).join(',')}\n\n`;
// Liabilities
csvContent += `LIABILITIES\n`;
LIABILITY_ITEMS_CONFIG.forEach((cfg) => {
const lineTot = getLineTotal(cfg.key);
const vals = BRANCHES_LIST.map((b) => getValue(b.id, cfg.key)).join(',');
csvContent += `"${cfg.label}",${lineTot.toFixed(1)},${vals}\n`;
});
const totLiab = BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalLiabilities(b.id), 0);
csvContent += `"Total Liabilities",${totLiab.toFixed(1)},${BRANCHES_LIST.map((b) => getBranchTotalLiabilities(b.id).toFixed(1)).join(',')}\n`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `Balance_Sheet_Consolidated_${period}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="space-y-6">
{/* Top Holographic Action & View Switcher Bar */}
<div className="holo-card p-4 rounded-2xl shadow-[0_0_30px_rgba(6,182,212,0.1)] flex flex-wrap items-center justify-between gap-4 border border-cyan-500/30">
<div className="flex items-center space-x-3">
<div className="p-2.5 bg-cyan-500/20 border border-cyan-400/50 rounded-xl text-cyan-300 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
<FileSpreadsheet className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-bold text-white flex items-center gap-2 glow-text-cyan">
Consolidated Network Matrix
<span className="text-xs bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-2.5 py-0.5 rounded-full font-mono shadow-[0_0_10px_rgba(16,185,129,0.3)]">
{BRANCHES_LIST.length}/{BRANCHES_LIST.length} SUBMITTED
</span>
</h2>
<p className="text-xs text-cyan-200/70 font-mono">
{viewMode === 'current'
? `REPORTING DATE: ${period}`
: viewMode === 'prior'
? `PRIOR DATE: ${priorPeriod}`
: `VARIANCE MATRIX: ${period} vs ${priorPeriod}`}
</p>
</div>
</div>
{/* View Mode Controls */}
<div className="flex flex-wrap items-center gap-3">
<div className="bg-slate-950/80 p-1 rounded-xl border border-cyan-500/30 flex items-center space-x-1 shadow-inner">
<button
onClick={() => setViewMode('current')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer font-mono ${
viewMode === 'current' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_#06b6d4] font-bold' : 'text-slate-400 hover:text-cyan-200'
}`}
>
Latest ({period})
</button>
<button
onClick={() => setViewMode('prior')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer font-mono ${
viewMode === 'prior' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_#06b6d4] font-bold' : 'text-slate-400 hover:text-cyan-200'
}`}
>
Previous ({priorPeriod})
</button>
<button
onClick={() => setViewMode('variance')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer font-mono flex items-center gap-1 ${
viewMode === 'variance' ? 'bg-amber-500 text-slate-950 shadow-[0_0_15px_#f59e0b] font-bold' : 'text-amber-400/90 hover:text-amber-300'
}`}
>
<TrendingUp className="w-3.5 h-3.5" />
<span>Variance View</span>
</button>
</div>
<div className="flex items-center space-x-2 border-l border-cyan-500/20 pl-3">
<button
onClick={() => setShowComments(!showComments)}
className={`px-3 py-1.5 rounded-xl text-xs font-mono font-medium border flex items-center space-x-1.5 transition-all cursor-pointer ${
showComments
? 'bg-cyan-500/20 border-cyan-400 text-cyan-300 shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-slate-950/80 border-slate-800 text-slate-400'
}`}
>
<MessageSquare className="w-3.5 h-3.5" />
<span>Comments Panel</span>
</button>
<button
onClick={exportToCSV}
className="px-3.5 py-1.5 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold rounded-xl text-xs flex items-center space-x-1.5 shadow-[0_0_15px_rgba(16,185,129,0.4)] transition-all cursor-pointer font-mono"
>
<Download className="w-3.5 h-3.5" />
<span>Export CSV</span>
</button>
</div>
</div>
</div>
{/* Main Grid + Comments Side-by-Side Layout */}
<div className={`grid grid-cols-1 ${showComments ? 'lg:grid-cols-4' : 'lg:grid-cols-1'} gap-6`}>
{/* Spreadsheet Matrix Section */}
<div className={`${showComments ? 'lg:col-span-3' : 'lg:col-span-1'} space-y-4`}>
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl">
{/* Table Header Banner (Replicating Spreadsheet Banner) */}
<div className="bg-gradient-to-r from-slate-950 via-slate-900 to-slate-950 px-6 py-3 border-b border-slate-800 flex justify-between items-center">
<h3 className="text-sm font-extrabold uppercase tracking-wider text-blue-200">
International Network Balance Sheet (USD Mn)
</h3>
<div className="text-xs font-mono font-bold text-amber-400 bg-amber-950/50 border border-amber-800/60 px-3 py-1 rounded-md">
{viewMode === 'variance' ? `VARIANCE (${period} vs ${priorPeriod})` : period}
</div>
</div>
<div className="overflow-x-auto overflow-y-auto max-h-[calc(100vh-220px)] relative rounded-xl border border-slate-800/80 shadow-2xl scrollbar-thin scrollbar-thumb-cyan-500/30">
<table className="w-full text-xs text-left border-separate border-spacing-0">
{/* Columns Header */}
<thead className="sticky top-0 z-30 bg-slate-950 shadow-md">
<tr className="bg-slate-950 text-slate-300 border-b border-slate-800">
<th className="py-2.5 px-3 font-bold border-r border-b border-slate-800 min-w-[210px] sticky top-0 left-0 z-40 bg-slate-950 shadow-r">
Line Items (USD Mn)
</th>
<th className="py-2.5 px-3 font-bold border-r border-b border-slate-800 text-right min-w-[80px] sticky top-0 z-30 bg-slate-900 text-amber-300">
Total
</th>
{activeBranchList.map((b) => (
<th key={b.id} className="py-2.5 px-2 font-bold border-r border-b border-slate-800 text-center min-w-[105px] sticky top-0 z-30 bg-slate-950">
<div className="text-xs font-bold text-white tracking-tight">{b.name}</div>
<div className="text-[9px] text-cyan-400/80 font-mono">{b.code}</div>
</th>
))}
{/* Closing branch header container */}
{closingBranchList.map((b) => (
<th
key={b.id}
className="py-2.5 px-2 font-bold border-r border-b border-slate-800 text-center min-w-[110px] sticky top-0 z-30 bg-blue-950/90 text-blue-200 border-t-2 border-t-blue-500"
title="Branches to be closed down"
>
<div className="text-[9px] text-blue-400 font-mono uppercase leading-tight">Closing Down</div>
<div className="text-xs font-bold text-white tracking-tight">{b.name}</div>
<div className="text-[9px] text-blue-300 font-mono">{b.code}</div>
</th>
))}
</tr>
</thead>
<tbody>
{/* ================= ASSETS SECTION ================= */}
<tr className="bg-slate-950/90 text-blue-300 font-bold border-b border-slate-800">
<td colSpan={2 + BRANCHES_LIST.length} className="py-1.5 px-3 uppercase text-[11px] tracking-wider sticky left-0 z-20 bg-slate-950">
Assets
</td>
</tr>
{ASSET_ITEMS_CONFIG.map((cfg, idx) => {
const lineTot = viewMode === 'variance' ? getVarianceTotal(cfg.key) : getLineTotal(cfg.key, viewMode === 'prior');
const isTopRow = idx < 3;
return (
<tr key={cfg.key} className="border-b border-slate-800/60 hover:bg-slate-800/40 transition-colors">
<td className="py-2 px-3 text-slate-200 border-r border-slate-800 font-medium sticky left-0 bg-slate-900 z-20">
{cfg.label}
</td>
<td className="py-2 px-3 font-bold border-r border-slate-800 text-right text-slate-100 bg-slate-900/60 font-mono">
{lineTot.toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const val = viewMode === 'variance' ? getVariance(b.id, cfg.key) : getValue(b.id, cfg.key, viewMode === 'prior');
const currVal = getValue(b.id, cfg.key, false);
const priorVal = getValue(b.id, cfg.key, true);
const varVal = currVal - priorVal;
const isExcessive = Math.abs(varVal) >= 2.0;
// Check Pledged styling
const isCentralBankPledged = cfg.key === 'balanceCentralBank' && (b.id === 'bangladesh' || b.id === 'japan');
const isInvestmentsPledged = cfg.key === 'investments' && (b.id === 'bangladesh' || b.id === 'saudi_arabia');
const sub = submissions[b.id];
const vc = sub?.varianceComments?.find((comment) => comment.lineItemKey === cfg.key);
let bgClass = '';
if (highlightPledged && isCentralBankPledged) bgClass = 'bg-amber-400/20 text-amber-200 font-semibold';
if (highlightPledged && isInvestmentsPledged) bgClass = 'bg-pink-500/20 text-pink-200 font-semibold';
let pledgedNote = '';
if (isCentralBankPledged) pledgedNote = 'Statutory Reserve / Central Bank Encumbered Asset';
if (isInvestmentsPledged) pledgedNote = 'Pledged Investment Holdings';
const hasNoteOrComment = Boolean(vc || pledgedNote || isExcessive);
return (
<td
key={b.id}
className={`py-2 px-2.5 border-r border-slate-800/60 text-right font-mono relative group hover:z-50 ${bgClass} ${
isExcessive && highlightExcessive ? 'border-2 border-red-500/80 bg-red-950/30 text-red-300 font-bold' : ''
}`}
>
<div className="flex items-center justify-end gap-1 cursor-help">
<span className={val < 0 ? 'text-rose-400' : ''}>
{val < 0 ? `(${Math.abs(val).toFixed(1)})` : val.toFixed(1)}
</span>
{hasNoteOrComment && (
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 shadow-[0_0_6px_#06b6d4] inline-block shrink-0 animate-pulse" />
)}
</div>
{/* Hover Bubble / Tooltip for Accessibility */}
<div className={`pointer-events-none group-hover:pointer-events-auto opacity-0 group-hover:opacity-100 transition-all duration-200 absolute z-[100] ${
isTopRow ? 'top-full mt-2' : 'bottom-full mb-2'
} right-0 w-72 p-3 bg-slate-950/95 border border-cyan-500/50 rounded-xl shadow-[0_0_30px_rgba(6,182,212,0.5)] text-left font-sans text-xs text-slate-100 backdrop-blur-xl`}>
<div className="flex items-center justify-between pb-1.5 mb-1.5 border-b border-cyan-500/20">
<span className="font-bold text-cyan-300 font-mono flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-cyan-400"></span>
{b.name} ({b.code})
</span>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-slate-300">
{period}
</span>
</div>
<div className="text-[11px] font-semibold text-slate-200 mb-1">
{cfg.label}
</div>
{vc ? (
<div className="p-2 rounded-lg bg-cyan-950/60 border border-cyan-500/30 text-cyan-200 text-[11px] mb-2 leading-relaxed">
<strong className="text-cyan-300 block text-[10px] uppercase font-mono mb-0.5">Variance Reason:</strong>
{vc.comment}
</div>
) : (
<div className="text-[11px] text-slate-400 italic mb-2">
Standard operational movement.
</div>
)}
{pledgedNote && (
<div className="p-1.5 rounded bg-amber-500/20 border border-amber-500/40 text-amber-300 text-[10px] font-mono mb-2">
{pledgedNote}
</div>
)}
{cfg.key === 'otherAssets' && sub?.compositions?.otherAssets && sub.compositions.otherAssets.length > 0 && (
<div className="p-2 rounded-lg bg-indigo-950/80 border border-indigo-500/40 text-indigo-200 text-[11px] mb-2 space-y-1">
<div className="flex items-center justify-between border-b border-indigo-500/30 pb-1 font-mono font-bold text-indigo-300 text-[10px] uppercase">
<span>Other Assets Composition</span>
<span>Amount</span>
</div>
<div className="space-y-1 max-h-32 overflow-y-auto pr-1 font-sans">
{sub.compositions.otherAssets.map((item) => (
<div key={item.id} className="flex justify-between text-[10px] items-center gap-2">
<span className="truncate text-slate-200">{item.description || 'Unspecified Component'}</span>
<span className="font-mono font-bold text-cyan-300 shrink-0">${item.amount.toFixed(1)}M</span>
</div>
))}
</div>
<div className="flex justify-between text-[10px] font-mono font-bold text-indigo-200 pt-1 border-t border-indigo-500/30">
<span>Sum Total:</span>
<span className="text-emerald-300">${sub.compositions.otherAssets.reduce((s, i) => s + (i.amount || 0), 0).toFixed(1)}M</span>
</div>
</div>
)}
<div className="grid grid-cols-3 gap-1 pt-1.5 border-t border-slate-800 text-[10px] font-mono text-center">
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">CURRENT</span>
<span className="font-bold text-cyan-300">${currVal.toFixed(1)}M</span>
</div>
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">PRIOR</span>
<span className="font-bold text-slate-300">${priorVal.toFixed(1)}M</span>
</div>
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">DELTA</span>
<span className={`font-bold ${varVal < 0 ? 'text-rose-400' : 'text-emerald-400'}`}>
{varVal >= 0 ? `+${varVal.toFixed(1)}` : varVal.toFixed(1)}M
</span>
</div>
</div>
</div>
</td>
);
})}
</tr>
);
})}
{/* TOTAL ASSETS ROW */}
<tr className="bg-blue-950/90 font-bold border-t-2 border-b-2 border-blue-500/50 text-white">
<td className="py-2.5 px-3 border-r border-slate-800 sticky left-0 bg-blue-950 uppercase tracking-wider text-[11px]">
Total Assets
</td>
<td className="py-2.5 px-3 border-r border-slate-800 text-right font-mono text-amber-300">
{BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalAssets(b.id, viewMode === 'prior'), 0).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const tot = getBranchTotalAssets(b.id, viewMode === 'prior');
return (
<td key={b.id} className="py-2.5 px-2.5 border-r border-slate-800/80 text-right font-mono text-blue-100">
{tot.toFixed(1)}
</td>
);
})}
</tr>
{/* ================= LIABILITIES SECTION ================= */}
<tr className="bg-slate-950/90 text-blue-300 font-bold border-b border-slate-800">
<td colSpan={2 + BRANCHES_LIST.length} className="py-1.5 px-3 uppercase text-[11px] tracking-wider pt-4 sticky left-0 z-20 bg-slate-950">
Liabilities
</td>
</tr>
{LIABILITY_ITEMS_CONFIG.map((cfg) => {
const lineTot = viewMode === 'variance' ? getVarianceTotal(cfg.key) : getLineTotal(cfg.key, viewMode === 'prior');
return (
<tr key={cfg.key} className="border-b border-slate-800/60 hover:bg-slate-800/40 transition-colors">
<td className="py-2 px-3 text-slate-200 border-r border-slate-800 font-medium sticky left-0 bg-slate-900 z-20">
{cfg.label}
</td>
<td className="py-2 px-3 font-bold border-r border-slate-800 text-right text-slate-100 bg-slate-900/60 font-mono">
{lineTot.toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const val = viewMode === 'variance' ? getVariance(b.id, cfg.key) : getValue(b.id, cfg.key, viewMode === 'prior');
const currVal = getValue(b.id, cfg.key, false);
const priorVal = getValue(b.id, cfg.key, true);
const varVal = currVal - priorVal;
const isExcessive = Math.abs(varVal) >= 2.0;
const sub = submissions[b.id];
const vc = sub?.varianceComments?.find((comment) => comment.lineItemKey === cfg.key);
const hasNoteOrComment = Boolean(vc || isExcessive);
return (
<td
key={b.id}
className={`py-2 px-2.5 border-r border-slate-800/60 text-right font-mono relative group hover:z-50 ${
isExcessive && highlightExcessive ? 'border-2 border-red-500/80 bg-red-950/30 text-red-300 font-bold' : ''
}`}
>
<div className="flex items-center justify-end gap-1 cursor-help">
<span className={val < 0 ? 'text-rose-400' : ''}>
{val < 0 ? `(${Math.abs(val).toFixed(1)})` : val.toFixed(1)}
</span>
{hasNoteOrComment && (
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 shadow-[0_0_6px_#06b6d4] inline-block shrink-0 animate-pulse" />
)}
</div>
{/* Hover Bubble / Tooltip for Accessibility */}
<div className="pointer-events-none group-hover:pointer-events-auto opacity-0 group-hover:opacity-100 transition-all duration-200 absolute z-[100] bottom-full right-0 mb-2 w-72 p-3 bg-slate-950/95 border border-cyan-500/50 rounded-xl shadow-[0_0_30px_rgba(6,182,212,0.5)] text-left font-sans text-xs text-slate-100 backdrop-blur-xl">
<div className="flex items-center justify-between pb-1.5 mb-1.5 border-b border-cyan-500/20">
<span className="font-bold text-cyan-300 font-mono flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-cyan-400"></span>
{b.name} ({b.code})
</span>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-slate-300">
{period}
</span>
</div>
<div className="text-[11px] font-semibold text-slate-200 mb-1">
{cfg.label}
</div>
{vc ? (
<div className="p-2 rounded-lg bg-cyan-950/60 border border-cyan-500/30 text-cyan-200 text-[11px] mb-2 leading-relaxed">
<strong className="text-cyan-300 block text-[10px] uppercase font-mono mb-0.5">Variance Reason:</strong>
{vc.comment}
</div>
) : (
<div className="text-[11px] text-slate-400 italic mb-2">
Standard operational movement.
</div>
)}
{cfg.key === 'otherLiabilities' && sub?.compositions?.otherLiabilities && sub.compositions.otherLiabilities.length > 0 && (
<div className="p-2 rounded-lg bg-indigo-950/80 border border-indigo-500/40 text-indigo-200 text-[11px] mb-2 space-y-1">
<div className="flex items-center justify-between border-b border-indigo-500/30 pb-1 font-mono font-bold text-indigo-300 text-[10px] uppercase">
<span>Other Liabilities Composition</span>
<span>Amount</span>
</div>
<div className="space-y-1 max-h-32 overflow-y-auto pr-1 font-sans">
{sub.compositions.otherLiabilities.map((item) => (
<div key={item.id} className="flex justify-between text-[10px] items-center gap-2">
<span className="truncate text-slate-200">{item.description || 'Unspecified Component'}</span>
<span className="font-mono font-bold text-cyan-300 shrink-0">${item.amount.toFixed(1)}M</span>
</div>
))}
</div>
<div className="flex justify-between text-[10px] font-mono font-bold text-indigo-200 pt-1 border-t border-indigo-500/30">
<span>Sum Total:</span>
<span className="text-emerald-300">${sub.compositions.otherLiabilities.reduce((s, i) => s + (i.amount || 0), 0).toFixed(1)}M</span>
</div>
</div>
)}
<div className="grid grid-cols-3 gap-1 pt-1.5 border-t border-slate-800 text-[10px] font-mono text-center">
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">CURRENT</span>
<span className="font-bold text-cyan-300">${currVal.toFixed(1)}M</span>
</div>
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">PRIOR</span>
<span className="font-bold text-slate-300">${priorVal.toFixed(1)}M</span>
</div>
<div className="bg-slate-900 p-1 rounded">
<span className="text-slate-400 block text-[9px]">DELTA</span>
<span className={`font-bold ${varVal < 0 ? 'text-rose-400' : 'text-emerald-400'}`}>
{varVal >= 0 ? `+${varVal.toFixed(1)}` : varVal.toFixed(1)}M
</span>
</div>
</div>
</div>
</td>
);
})}
</tr>
);
})}
{/* TOTAL LIABILITIES ROW */}
<tr className="bg-slate-950 font-bold border-t-2 border-b border-slate-700 text-slate-200">
<td className="py-2.5 px-3 border-r border-slate-800 sticky left-0 bg-slate-950 uppercase tracking-wider text-[11px]">
Total Liabilities (Incl. Other Liab)
</td>
<td className="py-2.5 px-3 border-r border-slate-800 text-right font-mono text-amber-300">
{BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalLiabilities(b.id, viewMode === 'prior'), 0).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const tot = getBranchTotalLiabilities(b.id, viewMode === 'prior');
return (
<td key={b.id} className="py-2.5 px-2.5 border-r border-slate-800 text-right font-mono text-slate-200">
{tot.toFixed(1)}
</td>
);
})}
</tr>
{/* NET ASSETS ROW */}
<tr className="bg-slate-900 font-extrabold text-white border-b border-slate-700">
<td className="py-2.5 px-3 border-r border-slate-800 sticky left-0 bg-slate-900 uppercase tracking-wider text-[11px] text-emerald-400">
Net Assets
</td>
<td className="py-2.5 px-3 border-r border-slate-800 text-right font-mono text-emerald-300">
{BRANCHES_LIST.reduce((sum, b) => sum + getBranchNetAssets(b.id, viewMode === 'prior'), 0).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const net = getBranchNetAssets(b.id, viewMode === 'prior');
return (
<td
key={b.id}
className={`py-2.5 px-2.5 border-r border-slate-800 text-right font-mono ${
net < 0 ? 'text-rose-400 font-bold' : 'text-emerald-300 font-bold'
}`}
>
{net < 0 ? `(${Math.abs(net).toFixed(1)})` : net.toFixed(1)}
</td>
);
})}
</tr>
{/* TOTAL LIABILITIES & EQUITY ROW */}
<tr className="bg-slate-950 font-bold text-slate-100 border-b-2 border-slate-700">
<td className="py-2.5 px-3 border-r border-slate-800 sticky left-0 bg-slate-950 uppercase tracking-wider text-[11px]">
Total Liabilities & Equity
</td>
<td className="py-2.5 px-3 border-r border-slate-800 text-right font-mono text-amber-300">
{BRANCHES_LIST.reduce(
(sum, b) => sum + (getBranchTotalLiabilities(b.id, viewMode === 'prior') + getBranchNetAssets(b.id, viewMode === 'prior')),
0
).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const totalLiabEq = getBranchTotalLiabilities(b.id, viewMode === 'prior') + getBranchNetAssets(b.id, viewMode === 'prior');
return (
<td key={b.id} className="py-2.5 px-2.5 border-r border-slate-800 text-right font-mono text-slate-200">
{totalLiabEq.toFixed(1)}
</td>
);
})}
</tr>
{/* ================= MANDATORY REQUIREMENTS / PLEDGES SECTION ================= */}
<tr className="bg-slate-950 text-slate-400 font-bold border-b border-slate-800">
<td colSpan={2 + BRANCHES_LIST.length} className="py-1.5 px-3 text-[11px] tracking-wider pt-3">
Mandatory Requirements (SLR, CRR and other mandatory requirements)
</td>
</tr>
{/* Full Pledged Row */}
<tr className="border-b border-slate-800/60 bg-pink-950/20">
<td className="py-2 px-3 text-pink-300 font-semibold border-r border-slate-800 sticky left-0 bg-slate-900 flex items-center space-x-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-pink-500 inline-block"></span>
<span>Full Pledged</span>
</td>
<td className="py-2 px-3 font-bold border-r border-slate-800 text-right text-pink-300 font-mono">
{BRANCHES_LIST.reduce((sum, b) => sum + getValue(b.id, 'fullPledged', viewMode === 'prior'), 0).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const val = getValue(b.id, 'fullPledged', viewMode === 'prior');
return (
<td key={b.id} className="py-2 px-2.5 border-r border-slate-800 text-right font-mono text-pink-300 font-semibold">
{val > 0 ? val.toFixed(1) : '-'}
</td>
);
})}
</tr>
{/* Partial Pledged Row */}
<tr className="border-b border-slate-800/60 bg-yellow-950/20">
<td className="py-2 px-3 text-yellow-300 font-semibold border-r border-slate-800 sticky left-0 bg-slate-900 flex items-center space-x-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-yellow-400 inline-block"></span>
<span>Partial Pledged</span>
</td>
<td className="py-2 px-3 font-bold border-r border-slate-800 text-right text-yellow-300 font-mono">
{BRANCHES_LIST.reduce((sum, b) => sum + getValue(b.id, 'partialPledged', viewMode === 'prior'), 0).toFixed(1)}
</td>
{BRANCHES_LIST.map((b) => {
const val = getValue(b.id, 'partialPledged', viewMode === 'prior');
return (
<td key={b.id} className="py-2 px-2.5 border-r border-slate-800 text-right font-mono text-yellow-300 font-semibold">
{val > 0 ? val.toFixed(1) : '-'}
</td>
);
})}
</tr>
{/* RECONCILIATION DIFFERENCE ROW */}
<tr className="bg-slate-950 border-t-2 border-slate-800 text-[11px] font-mono">
<td className="py-2 px-3 text-slate-400 font-bold border-r border-slate-800 sticky left-0 bg-slate-950">
Balance Reconciliation Difference
</td>
<td className="py-2 px-3 text-right text-emerald-400 font-bold border-r border-slate-800">
0.00
</td>
{BRANCHES_LIST.map((b) => {
const ta = getBranchTotalAssets(b.id, viewMode === 'prior');
const tl = getBranchTotalLiabilities(b.id, viewMode === 'prior');
const na = getBranchNetAssets(b.id, viewMode === 'prior');
const diff = Number((ta - (tl + na)).toFixed(2));
return (
<td
key={b.id}
className={`py-2 px-2.5 border-r border-slate-800 text-right font-bold ${
Math.abs(diff) > 0.01 ? 'text-rose-400 bg-rose-950/30' : 'text-emerald-400'
}`}
>
{diff === 0 ? '0.00' : diff.toFixed(2)}
</td>
);
})}
</tr>
</tbody>
</table>
</div>
{/* Table Footnote */}
<div className="bg-slate-950 px-4 py-2 border-t border-slate-800 text-[11px] text-slate-400 flex justify-between items-center">
<span>* Other Assets and Other Liabilities are approximate figures included in Total Assets and Total Liabilities respectively.</span>
<div className="flex items-center space-x-3 text-[10px]">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-pink-500"></span> Full Pledged</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-yellow-400"></span> Partial Pledged</span>
</div>
</div>
</div>
</div>
{/* Aggregate Variance Comments Sidebar (Image 3) */}
{showComments && (
<div className="lg:col-span-1 space-y-4">
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-xl space-y-3 sticky top-20">
<div className="flex items-center justify-between border-b border-slate-800 pb-2.5">
<div className="flex items-center space-x-2">
<MessageSquare className="w-4 h-4 text-blue-400" />
<h3 className="text-xs font-bold text-white uppercase tracking-wider">
Excessive Variance Explanations
</h3>
</div>
<span className="text-[10px] bg-blue-500/20 text-blue-300 border border-blue-500/30 px-2 py-0.5 rounded-full font-mono">
{allComments.length} Notes
</span>
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
Reasons provided by branches for line item variances exceeding <strong>USD 2.0 Mn</strong> between {period} and {priorPeriod}:
</p>
<div className="space-y-3 max-h-[680px] overflow-y-auto pr-1">
{allComments.length === 0 ? (
<div className="text-center py-8 text-slate-500 text-xs">
No variance reasons recorded for this period.
</div>
) : (
allComments.map((item, idx) => {
const b = BRANCHES_LIST.find((br) => br.id === item.branchId);
const vc = item.comment;
const isPositive = vc.variance >= 0;
return (
<div
key={idx}
className="bg-slate-950/80 border border-slate-800 hover:border-slate-700 rounded-lg p-3 space-y-1.5 transition-all text-xs"
>
<div className="flex justify-between items-start gap-1">
<span className="font-bold text-blue-300 text-[11px]">
{b?.name || item.branchId}:
</span>
<span
className={`font-mono text-[11px] px-1.5 py-0.2 rounded ${
isPositive ? 'bg-emerald-950 text-emerald-300 border border-emerald-800/50' : 'bg-rose-950 text-rose-300 border border-rose-800/50'
}`}
>
{isPositive ? `+${vc.variance.toFixed(1)}M` : `${vc.variance.toFixed(1)}M`}
</span>
</div>
<div className="text-[11px] font-semibold text-slate-200">
{vc.lineItemLabel}
</div>
<p className="text-[11px] text-slate-300 bg-slate-900/90 p-2 rounded border border-slate-800/80 italic leading-snug">
"{vc.comment}"
</p>
</div>
);
})
)}
</div>
</div>
</div>
)}
</div>
</div>
);
};

View file

@ -0,0 +1,245 @@
import React from 'react';
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
PieChart,
Pie,
Cell,
CartesianGrid,
AreaChart,
Area
} from 'recharts';
import {
Globe2,
TrendingUp,
Building2,
DollarSign,
PieChart as PieIcon,
ShieldCheck,
CheckCircle2
} from 'lucide-react';
import {
BRANCHES_LIST,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
BranchSubmission,
BranchId
} from '../types';
import { InteractiveWorldMap } from './InteractiveWorldMap';
interface DashboardAnalyticsProps {
period: string;
submissions: Record<BranchId, BranchSubmission>;
}
const COLORS = [
'#3b82f6', // blue
'#10b981', // emerald
'#f59e0b', // amber
'#8b5cf6', // purple
'#ec4899', // pink
'#06b6d4', // cyan
'#f97316', // orange
'#6366f1', // indigo
];
export const DashboardAnalytics: React.FC<DashboardAnalyticsProps> = ({ period, submissions }) => {
// 1. Data for Branch Assets vs Liabilities Bar Chart
const branchBarData = BRANCHES_LIST.map((b) => {
const sub = submissions[b.id];
const items = sub?.items;
let totAssets = 0;
let totLiab = 0;
if (items) {
totAssets = ASSET_ITEMS_CONFIG.reduce((sum, cfg) => sum + (items[cfg.key] || 0), 0);
totLiab = LIABILITY_ITEMS_CONFIG.reduce((sum, cfg) => sum + (items[cfg.key] || 0), 0);
}
return {
name: b.code,
fullName: b.name,
Assets: Number(totAssets.toFixed(1)),
Liabilities: Number(totLiab.toFixed(1)),
NetAssets: Number((totAssets - totLiab).toFixed(1)),
};
});
// 2. Data for Asset Category Composition Pie Chart
const assetCategoryTotals: Record<string, number> = {};
ASSET_ITEMS_CONFIG.forEach((cfg) => {
let tot = 0;
BRANCHES_LIST.forEach((b) => {
tot += submissions[b.id]?.items[cfg.key] || 0;
});
assetCategoryTotals[cfg.label] = Number(tot.toFixed(1));
});
const pieData = Object.keys(assetCategoryTotals).map((label) => ({
name: label,
value: assetCategoryTotals[label],
}));
// Totals
const grandTotalAssets = branchBarData.reduce((acc, b) => acc + b.Assets, 0);
const grandTotalLiabilities = branchBarData.reduce((acc, b) => acc + b.Liabilities, 0);
const grandNetAssets = grandTotalAssets - grandTotalLiabilities;
return (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Top Executive Summary Bar */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-3">
<div className="p-3 bg-indigo-600/20 border border-indigo-500/30 rounded-xl text-indigo-400">
<Globe2 className="w-6 h-6" />
</div>
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
Consolidated Executive Financial Visuals
<span className="text-xs bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 px-2 py-0.5 rounded font-mono">
{period}
</span>
</h2>
<p className="text-xs text-slate-400">
Interactive analytics on balance sheet scale, asset distribution, and entity capital position.
</p>
</div>
</div>
<div className="flex items-center space-x-2 text-xs text-slate-300 font-mono bg-slate-950 px-3 py-1.5 rounded-lg border border-slate-800">
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
<span>100% Entities Reconciled</span>
</div>
</div>
{/* KPI Highlight Cards with High Density Left Border Accents */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-slate-900 border border-slate-800 rounded-lg p-4 shadow space-y-1 border-l-4 border-l-blue-500">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block">
Total Consolidated Assets
</span>
<div className="text-xl font-extrabold text-white font-mono">
${grandTotalAssets.toFixed(1)}M
</div>
<span className="text-[10px] text-emerald-400 font-semibold block mt-1">+2.4% vs Prev Period</span>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-lg p-4 shadow space-y-1 border-l-4 border-l-emerald-500">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block">
Placement / Borrow Check
</span>
<div className="text-xl font-extrabold text-emerald-400 font-mono">
Equalized
</div>
<span className="text-[10px] text-slate-400 block mt-1">Δ Diff: $0.00M (Balanced)</span>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-lg p-4 shadow space-y-1 border-l-4 border-l-amber-500">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block">
Active Variances (&gt;$2M)
</span>
<div className="text-xl font-extrabold text-amber-400 font-mono">
03 Active
</div>
<span className="text-[10px] text-amber-500 font-bold uppercase block mt-1">Requires HO Review</span>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-lg p-4 shadow space-y-1 border-l-4 border-l-purple-500">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block">
Reconciliation Health
</span>
<div className="text-xl font-extrabold text-white font-mono">
100.0%
</div>
<div className="w-full h-1 bg-slate-800 rounded-full mt-2 overflow-hidden">
<div className="bg-blue-500 h-full w-full"></div>
</div>
</div>
</div>
{/* Interactive Global World Map View */}
<InteractiveWorldMap period={period} submissions={submissions} />
{/* Charts Row 1: Branch Assets vs Liabilities + Asset Composition Pie */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Assets vs Liabilities Bar Chart */}
<div className="lg:col-span-2 bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-400" />
Branch Balance Sheet Scale (Assets vs Liabilities)
</h3>
<span className="text-[11px] text-slate-400 font-mono">USD Mn</span>
</div>
<div className="h-80 w-full pt-2">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={branchBarData} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.5} />
<XAxis dataKey="name" stroke="#94a3b8" fontSize={11} />
<YAxis stroke="#94a3b8" fontSize={11} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', color: '#fff' }}
/>
<Legend wrapperStyle={{ fontSize: '11px', paddingTop: '8px' }} />
<Bar dataKey="Assets" fill="#3b82f6" radius={[4, 4, 0, 0]} />
<Bar dataKey="Liabilities" fill="#64748b" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Asset Composition Pie Chart */}
<div className="lg:col-span-1 bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-4">
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<PieIcon className="w-4 h-4 text-amber-400" />
Network Asset Portfolio
</h3>
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius={55}
outerRadius={85}
paddingAngle={3}
dataKey="value"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', color: '#fff', fontSize: '11px' }}
/>
</PieChart>
</ResponsiveContainer>
</div>
<div className="space-y-1.5 max-h-36 overflow-y-auto text-[11px]">
{pieData.map((item, idx) => (
<div key={idx} className="flex justify-between items-center text-slate-300">
<span className="flex items-center gap-2 truncate max-w-[170px]">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: COLORS[idx % COLORS.length] }}></span>
<span className="truncate">{item.name}</span>
</span>
<span className="font-mono font-bold text-slate-100">${item.value}M</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,339 @@
import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { Sparkles, Eye, Zap, Volume2, VolumeX, Sliders, Shield } from 'lucide-react';
export type HolographicThemeMode = 'cyan' | 'synthwave' | 'matrix' | 'amber';
interface HolographicCanvasProps {
themeMode?: HolographicThemeMode;
onThemeChange?: (mode: HolographicThemeMode) => void;
}
export const HolographicCanvas: React.FC<HolographicCanvasProps> = ({
themeMode = 'cyan',
onThemeChange
}) => {
const mountRef = useRef<HTMLDivElement>(null);
const [currentTheme, setCurrentTheme] = useState<HolographicThemeMode>(themeMode);
const [soundEnabled, setSoundEnabled] = useState(false);
const [particleDensity, setParticleDensity] = useState<'high' | 'ultra'>('high');
const [hudVisible, setHudVisible] = useState(true);
// Web Audio synth effect for sci-fi hover clicks
const playSciFiBeep = (freq = 440, type: OscillatorType = 'sine') => {
if (!soundEnabled) return;
try {
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioCtx) return;
const ctx = new AudioCtx();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(0.05, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.15);
} catch (e) {
// ignore audio error
}
};
useEffect(() => {
const container = mountRef.current;
if (!container) return;
// 1. Scene & Camera Setup
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x030712, 0.015);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 0, 45);
// 2. Renderer Setup safely wrapped in try-catch for iframe/browser compatibility
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x030712, 1);
container.appendChild(renderer.domElement);
} catch (e) {
console.warn('WebGL is not supported or context creation failed:', e);
return;
}
// Theme Color Map
const colorMap = {
cyan: { primary: 0x06b6d4, secondary: 0x3b82f6, accent: 0xa855f7 },
synthwave: { primary: 0xec4899, secondary: 0x8b5cf6, accent: 0x06b6d4 },
matrix: { primary: 0x10b981, secondary: 0x059669, accent: 0x34d399 },
amber: { primary: 0xf59e0b, secondary: 0xf97316, accent: 0xef4444 }
};
const currentColors = colorMap[currentTheme];
// 3. Particles Mesh
const particleCount = particleDensity === 'ultra' ? 1200 : 700;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const scales = new Float32Array(particleCount);
const colorObj = new THREE.Color(currentColors.primary);
const secondaryColorObj = new THREE.Color(currentColors.secondary);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 120;
positions[i * 3 + 1] = (Math.random() - 0.5) * 120;
positions[i * 3 + 2] = (Math.random() - 0.5) * 120;
const mixRatio = Math.random();
const mixedColor = colorObj.clone().lerp(secondaryColorObj, mixRatio);
colors[i * 3] = mixedColor.r;
colors[i * 3 + 1] = mixedColor.g;
colors[i * 3 + 2] = mixedColor.b;
scales[i] = Math.random() * 2 + 0.5;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const pMaterial = new THREE.PointsMaterial({
size: 1.2,
vertexColors: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(geometry, pMaterial);
scene.add(particleSystem);
// 4. Abstract Floating Holographic Geometric Polyhedrons
const polyGroup = new THREE.Group();
// Wireframe Icosahedron
const icoGeom = new THREE.IcosahedronGeometry(12, 1);
const icoMat = new THREE.MeshBasicMaterial({
color: currentColors.primary,
wireframe: true,
transparent: true,
opacity: 0.25
});
const icoMesh = new THREE.Mesh(icoGeom, icoMat);
icoMesh.position.set(-25, 10, -10);
polyGroup.add(icoMesh);
// Inner glowing core
const coreGeom = new THREE.OctahedronGeometry(6, 0);
const coreMat = new THREE.MeshBasicMaterial({
color: currentColors.accent,
wireframe: true,
transparent: true,
opacity: 0.45
});
const coreMesh = new THREE.Mesh(coreGeom, coreMat);
icoMesh.add(coreMesh);
// Torus Ring Matrix
const torusGeom = new THREE.TorusGeometry(18, 0.2, 16, 100);
const torusMat = new THREE.MeshBasicMaterial({
color: currentColors.secondary,
wireframe: true,
transparent: true,
opacity: 0.3
});
const torusMesh = new THREE.Mesh(torusGeom, torusMat);
torusMesh.position.set(28, -12, -15);
torusMesh.rotation.x = Math.PI / 3;
polyGroup.add(torusMesh);
// Dynamic Cyber Grid Plane
const gridHelper = new THREE.GridHelper(160, 40, currentColors.primary, 0x1e293b);
gridHelper.position.y = -30;
gridHelper.material.transparent = true;
gridHelper.material.opacity = 0.25;
scene.add(gridHelper);
scene.add(polyGroup);
// 5. Mouse Parallax & Smooth Interpolation
let mouseX = 0;
let mouseY = 0;
let targetX = 0;
let targetY = 0;
const onMouseMove = (e: MouseEvent) => {
mouseX = (e.clientX - window.innerWidth / 2) * 0.0008;
mouseY = (e.clientY - window.innerHeight / 2) * 0.0008;
};
window.addEventListener('mousemove', onMouseMove);
// Window Resize handler
const onResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onResize);
// 6. Animation Loop
let clock = new THREE.Clock();
let animationFrameId: number;
const animate = () => {
const elapsedTime = clock.getElapsedTime();
// Rotate particle cloud gently
particleSystem.rotation.y = elapsedTime * 0.03;
particleSystem.rotation.x = elapsedTime * 0.015;
// Rotate polyhedrons
icoMesh.rotation.x = elapsedTime * 0.2;
icoMesh.rotation.y = elapsedTime * 0.3;
coreMesh.rotation.y = -elapsedTime * 0.5;
torusMesh.rotation.z = elapsedTime * 0.15;
torusMesh.rotation.y = elapsedTime * 0.2;
// Mouse camera parallax smooth interpolation
targetX += (mouseX - targetX) * 0.05;
targetY += (mouseY - targetY) * 0.05;
camera.position.x = Math.sin(targetX) * 45;
camera.position.z = Math.cos(targetX) * 45;
camera.position.y = -targetY * 30;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
animationFrameId = requestAnimationFrame(animate);
};
animate();
// Clean up
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('resize', onResize);
cancelAnimationFrame(animationFrameId);
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
geometry.dispose();
pMaterial.dispose();
icoGeom.dispose();
icoMat.dispose();
torusGeom.dispose();
torusMat.dispose();
renderer.dispose();
};
}, [currentTheme, particleDensity]);
const handleThemeChange = (mode: HolographicThemeMode) => {
setCurrentTheme(mode);
playSciFiBeep(880, 'sine');
if (onThemeChange) onThemeChange(mode);
};
return (
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
{/* Three.js Canvas Container */}
<div ref={mountRef} className="absolute inset-0 w-full h-full" />
{/* Cyber Scanline Overlay */}
<div className="absolute inset-0 scanlines opacity-30 pointer-events-none" />
{/* Ambient Radial Gradient Lighting */}
<div className="absolute inset-0 bg-radial from-transparent via-slate-950/60 to-slate-950/95 pointer-events-none" />
{/* Floating HUD Control Overlay bar (top right corner) */}
<div className="absolute top-3 right-3 sm:right-6 pointer-events-auto z-40 flex items-center space-x-2 bg-slate-900/80 border border-cyan-500/40 rounded-xl p-1.5 backdrop-blur-xl shadow-[0_0_20px_rgba(6,182,212,0.2)]">
<button
onClick={() => {
setHudVisible(!hudVisible);
playSciFiBeep(600);
}}
className="p-1.5 text-cyan-400 hover:bg-cyan-500/20 rounded-lg transition-colors cursor-pointer"
title="Toggle Holographic HUD Matrix"
>
<Eye className="w-4 h-4" />
</button>
{hudVisible && (
<div className="flex items-center space-x-2 pl-1 pr-1 border-l border-slate-800 text-xs font-mono">
{/* Theme Mode Selector */}
<div className="flex items-center space-x-1">
<button
onClick={() => handleThemeChange('cyan')}
className={`w-4 h-4 rounded-full border transition-all cursor-pointer ${
currentTheme === 'cyan' ? 'bg-cyan-400 border-white scale-110 shadow-[0_0_10px_#06b6d4]' : 'bg-cyan-900 border-cyan-700'
}`}
title="Quantum Cyan Universe"
/>
<button
onClick={() => handleThemeChange('synthwave')}
className={`w-4 h-4 rounded-full border transition-all cursor-pointer ${
currentTheme === 'synthwave' ? 'bg-pink-500 border-white scale-110 shadow-[0_0_10px_#ec4899]' : 'bg-pink-950 border-pink-800'
}`}
title="Synthwave Neon Matrix"
/>
<button
onClick={() => handleThemeChange('matrix')}
className={`w-4 h-4 rounded-full border transition-all cursor-pointer ${
currentTheme === 'matrix' ? 'bg-emerald-400 border-white scale-110 shadow-[0_0_10px_#10b981]' : 'bg-emerald-950 border-emerald-800'
}`}
title="Deep Matrix Emerald"
/>
<button
onClick={() => handleThemeChange('amber')}
className={`w-4 h-4 rounded-full border transition-all cursor-pointer ${
currentTheme === 'amber' ? 'bg-amber-400 border-white scale-110 shadow-[0_0_10px_#f59e0b]' : 'bg-amber-950 border-amber-800'
}`}
title="Amber Tactical HUD"
/>
</div>
<div className="w-px h-4 bg-slate-800" />
{/* Density toggle */}
<button
onClick={() => {
setParticleDensity(particleDensity === 'high' ? 'ultra' : 'high');
playSciFiBeep(720);
}}
className="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 hover:bg-slate-700 text-cyan-300 border border-cyan-500/30 uppercase cursor-pointer"
>
Particles: {particleDensity}
</button>
{/* Audio Feedback toggle */}
<button
onClick={() => {
setSoundEnabled(!soundEnabled);
playSciFiBeep( soundEnabled ? 300 : 800);
}}
className={`p-1 rounded transition-colors cursor-pointer ${
soundEnabled ? 'text-cyan-400 bg-cyan-500/20 border border-cyan-500/40' : 'text-slate-500 hover:text-slate-300'
}`}
title={soundEnabled ? 'Audio Pulse Feedback On' : 'Audio Pulse Feedback Off'}
>
{soundEnabled ? <Volume2 className="w-3.5 h-3.5" /> : <VolumeX className="w-3.5 h-3.5" />}
</button>
</div>
)}
</div>
</div>
);
};

View file

@ -0,0 +1,430 @@
import React, { useState } from 'react';
import {
BRANCHES_LIST,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
BranchSubmission,
BranchId,
BranchInfo
} from '../types';
import {
Globe2,
Building2,
AlertTriangle,
CheckCircle2,
Lock,
ArrowRight,
TrendingUp,
ShieldAlert,
Sparkles
} from 'lucide-react';
interface InteractiveWorldMapProps {
period: string;
submissions: Record<BranchId, BranchSubmission>;
onSelectBranch?: (branchId: BranchId) => void;
}
// Map Branch IDs to geographic map coordinates on 1000x500 canvas
const BRANCH_COORDINATES: Record<BranchId, { x: number; y: number; flag: string; region: string }> = {
germany: { x: 495, y: 145, flag: '🇩🇪', region: 'Europe' },
saudi_arabia: { x: 600, y: 225, flag: '🇸🇦', region: 'Middle East' },
bahrain: { x: 628, y: 215, flag: '🇧🇭', region: 'Middle East' },
afghanistan: { x: 665, y: 195, flag: '🇦🇫', region: 'South Asia' },
bangladesh: { x: 730, y: 225, flag: '🇧🇩', region: 'South Asia' },
epz: { x: 755, y: 238, flag: '🇧🇩', region: 'EPZ Region' },
hk: { x: 805, y: 242, flag: '🇭🇰', region: 'East Asia' },
south_korea: { x: 845, y: 180, flag: '🇰🇷', region: 'East Asia' },
japan: { x: 885, y: 172, flag: '🇯🇵', region: 'East Asia' },
};
export const InteractiveWorldMap: React.FC<InteractiveWorldMapProps> = ({
period,
submissions,
onSelectBranch
}) => {
const [hoveredBranchId, setHoveredBranchId] = useState<BranchId | null>('bahrain');
const [selectedBranchId, setSelectedBranchId] = useState<BranchId | null>(null);
const activeBranchId = hoveredBranchId || selectedBranchId || 'bahrain';
const activeBranch = BRANCHES_LIST.find((b) => b.id === activeBranchId) || BRANCHES_LIST[0];
const activeSubmission = submissions[activeBranch.id];
const activeItems = activeSubmission?.items;
// Calculate detailed financial breakdown for active branch
let totAssets = 0;
let totLiab = 0;
if (activeItems) {
totAssets = ASSET_ITEMS_CONFIG.reduce((sum, cfg) => sum + (activeItems[cfg.key] || 0), 0);
totLiab = LIABILITY_ITEMS_CONFIG.reduce((sum, cfg) => sum + (activeItems[cfg.key] || 0), 0);
}
const netEquity = totAssets - totLiab;
// Key line items
const balancesWithBanks = activeItems?.balancesWithBanks || 0;
const centralBank = activeItems?.balanceCentralBank || 0;
const investments = activeItems?.investments || 0;
const advances = activeItems?.netAdvances || 0;
const customerDeposits = activeItems?.customerDeposit || 0;
const fullPledged = activeItems?.fullPledged || 0;
const partialPledged = activeItems?.partialPledged || 0;
// Active variance comments count
const varianceCommentsCount = activeSubmission?.varianceComments?.length || 0;
return (
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-2xl space-y-4 relative overflow-hidden">
{/* Background glow effects */}
<div className="absolute top-0 right-1/4 w-96 h-96 bg-cyan-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 left-1/4 w-96 h-96 bg-indigo-500/5 rounded-full blur-3xl pointer-events-none" />
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-800/80 pb-4 relative z-10">
<div className="flex items-center space-x-3">
<div className="p-2.5 bg-cyan-500/10 border border-cyan-500/30 rounded-xl text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.15)]">
<Globe2 className="w-5 h-5 animate-pulse" />
</div>
<div>
<h3 className="text-base font-bold text-white flex items-center gap-2">
Global Branch Operations & Balance Sheet Map
<span className="text-[10px] bg-cyan-500/20 text-cyan-300 border border-cyan-500/40 px-2 py-0.5 rounded-full font-mono">
Interactive GUI
</span>
</h3>
<p className="text-xs text-slate-400">
Hover or tap any international branch node to view live balance sheet metrics and statutory position.
</p>
</div>
</div>
<div className="flex items-center space-x-2 text-xs font-mono text-slate-300 bg-slate-950 px-3 py-1.5 rounded-lg border border-slate-800">
<Sparkles className="w-3.5 h-3.5 text-cyan-400" />
<span>Active Entity: <strong className="text-cyan-300">{activeBranch.name}</strong></span>
</div>
</div>
{/* Map & Detail Split Layout */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5 relative z-10">
{/* Left Column: Interactive Vector World Map Canvas (8 cols) */}
<div className="lg:col-span-8 bg-slate-950/80 border border-slate-800/90 rounded-xl p-4 relative min-h-[380px] flex flex-col justify-between overflow-hidden shadow-inner group">
{/* Subtle Grid Lines Overlay */}
<div
className="absolute inset-0 opacity-15 pointer-events-none"
style={{
backgroundImage: 'radial-gradient(#38bdf8 1px, transparent 1px)',
backgroundSize: '24px 24px'
}}
/>
{/* SVG World Canvas */}
<div className="relative w-full aspect-[1000/480] my-auto">
<svg
viewBox="0 0 1000 480"
className="w-full h-full select-none"
style={{ filter: 'drop-shadow(0 0 10px rgba(15,23,42,0.8))' }}
>
{/* Latitude & Longitude Tactical Grid Overlay */}
<g stroke="#1e293b" strokeWidth="0.5" strokeDasharray="2,4" opacity="0.6">
{/* Equator */}
<line x1="0" y1="240" x2="1000" y2="240" stroke="#06b6d4" strokeWidth="0.8" opacity="0.4" />
{/* Tropics */}
<line x1="0" y1="160" x2="1000" y2="160" />
<line x1="0" y1="320" x2="1000" y2="320" />
{/* Meridians */}
<line x1="200" y1="0" x2="200" y2="480" />
<line x1="400" y1="0" x2="400" y2="480" />
<line x1="600" y1="0" x2="600" y2="480" stroke="#38bdf8" strokeWidth="0.8" opacity="0.3" />
<line x1="800" y1="0" x2="800" y2="480" />
</g>
{/* Detailed World Map Continents Silhouettes */}
<g fill="#1e293b" opacity="0.85" stroke="#38bdf8" strokeWidth="0.75" className="transition-all duration-300">
{/* North America */}
<path d="M 60 70 L 90 50 L 140 40 L 210 35 L 250 45 L 290 55 L 320 80 L 300 110 L 260 130 L 250 150 L 230 180 L 240 210 L 220 220 L 210 200 L 190 190 L 170 160 L 140 160 L 120 180 L 100 150 L 70 120 L 50 90 Z" fill="#1e293b" stroke="#334155" />
{/* Greenland */}
<path d="M 320 25 L 370 20 L 390 45 L 350 65 L 330 50 Z" fill="#1e293b" stroke="#334155" />
{/* Central America & Caribbean */}
<path d="M 230 215 L 260 235 L 275 255 L 265 260 L 245 235 Z" fill="#1e293b" stroke="#334155" />
{/* South America */}
<path d="M 270 260 L 300 250 L 335 270 L 360 300 L 370 340 L 350 380 L 325 430 L 305 445 L 295 410 L 280 360 L 265 310 L 255 280 Z" fill="#1e293b" stroke="#334155" />
{/* Europe */}
<path d="M 450 60 L 480 50 L 520 45 L 560 60 L 580 85 L 565 110 L 530 125 L 500 145 L 470 155 L 450 140 L 440 110 L 435 85 Z" fill="#1e293b" stroke="#334155" />
{/* British Isles */}
<path d="M 430 75 L 445 70 L 440 95 L 425 90 Z" fill="#1e293b" stroke="#334155" />
{/* Scandinavia */}
<path d="M 490 35 L 520 25 L 535 55 L 510 70 L 495 50 Z" fill="#1e293b" stroke="#334155" />
{/* Africa */}
<path d="M 450 160 L 520 155 L 580 175 L 610 215 L 600 255 L 575 295 L 550 350 L 525 385 L 500 370 L 475 310 L 455 270 L 440 220 L 435 185 Z" fill="#1e293b" stroke="#334155" />
{/* Madagascar */}
<path d="M 605 320 L 620 315 L 615 360 L 600 365 Z" fill="#1e293b" stroke="#334155" />
{/* Asia / Eurasia */}
<path d="M 575 60 L 650 40 L 750 35 L 870 45 L 940 70 L 960 110 L 920 140 L 890 145 L 850 135 L 820 165 L 780 180 L 760 230 L 730 240 L 700 245 L 670 230 L 650 200 L 610 205 L 585 185 L 570 140 Z" fill="#1e293b" stroke="#334155" />
{/* Arabian Peninsula */}
<path d="M 585 205 L 635 195 L 650 235 L 620 260 L 590 245 Z" fill="#1e293b" stroke="#334155" />
{/* Indian Subcontinent */}
<path d="M 670 215 L 725 210 L 745 240 L 720 280 L 690 260 L 675 230 Z" fill="#1e293b" stroke="#334155" />
{/* Indochina & SE Asia */}
<path d="M 750 225 L 790 220 L 805 260 L 775 285 L 755 250 Z" fill="#1e293b" stroke="#334155" />
{/* Japanese Archipelago */}
<path d="M 875 145 L 900 150 L 895 190 L 870 185 Z" fill="#1e293b" stroke="#334155" />
{/* Korean Peninsula */}
<path d="M 835 165 L 855 165 L 850 195 L 835 190 Z" fill="#1e293b" stroke="#334155" />
{/* Australia & Oceania */}
<path d="M 780 320 L 840 305 L 900 320 L 910 365 L 880 405 L 820 410 L 775 375 L 770 340 Z" fill="#1e293b" stroke="#334155" />
{/* Indonesia / Philippines */}
<path d="M 770 285 L 830 275 L 860 295 L 810 305 L 765 295 Z" fill="#1e293b" stroke="#334155" />
{/* New Zealand */}
<path d="M 930 400 L 945 395 L 935 435 L 920 430 Z" fill="#1e293b" stroke="#334155" />
</g>
{/* Head Office / Regional Hub Arcs */}
{BRANCHES_LIST.map((b) => {
const coords = BRANCH_COORDINATES[b.id];
if (!coords) return null;
const isHovered = b.id === activeBranchId;
// Central Hub (Karachi / HO - approx x:670, y:210)
const hoX = 672;
const hoY = 208;
const midX = (hoX + coords.x) / 2;
const midY = Math.min(hoY, coords.y) - 30;
return (
<g key={`arc-${b.id}`}>
<path
d={`M ${hoX} ${hoY} Q ${midX} ${midY} ${coords.x} ${coords.y}`}
fill="none"
stroke={isHovered ? '#06b6d4' : '#334155'}
strokeWidth={isHovered ? 2 : 0.8}
strokeDasharray={isHovered ? '4,4' : 'none'}
opacity={isHovered ? 1 : 0.4}
className={isHovered ? 'animate-pulse' : ''}
/>
</g>
);
})}
{/* Central Head Office Node */}
<g transform="translate(672, 208)">
<circle r="8" fill="#3b82f6" opacity="0.3" className="animate-ping" />
<circle r="4" fill="#3b82f6" stroke="#93c5fd" strokeWidth="1.5" />
<text y="-10" textAnchor="middle" fill="#93c5fd" fontSize="9" fontWeight="bold" fontFamily="monospace">
HO
</text>
</g>
{/* Interactive Branch Pins */}
{BRANCHES_LIST.map((b) => {
const coords = BRANCH_COORDINATES[b.id];
if (!coords) return null;
const isHovered = b.id === activeBranchId;
const isClosing = b.isClosingDown;
// Color based on status
let pinColor = '#06b6d4'; // Cyan default
if (isClosing) pinColor = '#3b82f6'; // Blue
else if (b.id === 'bangladesh' || b.id === 'saudi_arabia') pinColor = '#f59e0b'; // Amber pledged/variance
return (
<g
key={b.id}
transform={`translate(${coords.x}, ${coords.y})`}
className="cursor-pointer transition-all duration-300"
onMouseEnter={() => setHoveredBranchId(b.id)}
onClick={() => {
setSelectedBranchId(b.id);
if (onSelectBranch) onSelectBranch(b.id);
}}
>
{/* Pulsing Outer Glow ring if active */}
{isHovered && (
<circle
r="14"
fill={pinColor}
opacity="0.25"
className="animate-ping"
/>
)}
{/* Outer Ring */}
<circle
r={isHovered ? '8' : '5'}
fill="#020617"
stroke={pinColor}
strokeWidth={isHovered ? '2.5' : '1.5'}
className="transition-all duration-200"
/>
{/* Inner Core */}
<circle
r={isHovered ? '4' : '2.5'}
fill={pinColor}
/>
{/* Label Tag */}
<g transform={`translate(0, ${isHovered ? -14 : -10})`}>
<rect
x="-20"
y="-11"
width="40"
height="13"
rx="3"
fill="#020617"
fillOpacity="0.85"
stroke={isHovered ? pinColor : '#334155'}
strokeWidth="0.75"
/>
<text
x="0"
y="-2"
textAnchor="middle"
fill={isHovered ? '#ffffff' : '#94a3b8'}
fontSize="8"
fontWeight="bold"
fontFamily="monospace"
>
{b.code}
</text>
</g>
</g>
);
})}
</svg>
</div>
{/* Map Legend */}
<div className="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800/80 text-[10px] font-mono text-slate-400">
<div className="flex items-center space-x-4">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-cyan-400 shadow-[0_0_8px_#06b6d4]"></span>
Standard Operational Branch
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-amber-400 shadow-[0_0_8px_#f59e0b]"></span>
Pledged Asset / Variance Alert
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-blue-500 shadow-[0_0_8px_#3b82f6]"></span>
Winding Down Entity
</span>
</div>
<span className="text-slate-500">Tap pin to lock inspection focus</span>
</div>
</div>
{/* Right Column: Intuitive GUI Balance Sheet Card Overview (4 cols) */}
<div className="lg:col-span-4 bg-slate-950 border border-cyan-500/30 rounded-xl p-4 shadow-[0_0_25px_rgba(6,182,212,0.15)] flex flex-col justify-between space-y-4 relative overflow-hidden">
{/* Subtle top cyan line */}
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-cyan-500 via-indigo-500 to-blue-500" />
{/* Branch Title & Status Header */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-2xl" role="img" aria-label="country flag">
{BRANCH_COORDINATES[activeBranch.id]?.flag || '🌐'}
</span>
<span
className={`text-[10px] font-mono font-bold px-2.5 py-0.5 rounded-full border ${
activeBranch.isClosingDown
? 'bg-blue-950 text-blue-300 border-blue-500/50'
: 'bg-emerald-950 text-emerald-300 border-emerald-500/50'
}`}
>
{activeBranch.isClosingDown ? 'Winding Down Entity' : '100% Reconciled'}
</span>
</div>
<div>
<h4 className="text-base font-extrabold text-white tracking-tight flex items-center gap-1.5">
{activeBranch.name}
</h4>
<p className="text-xs text-cyan-300/80 font-mono">
{BRANCH_COORDINATES[activeBranch.id]?.region} Code: {activeBranch.code}
</p>
</div>
</div>
{/* Quick Balance Sheet Numbers KPI Grid */}
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
<div className="bg-slate-900/90 border border-slate-800 p-2.5 rounded-lg space-y-0.5">
<span className="text-[10px] text-slate-400 block font-sans">TOTAL ASSETS</span>
<span className="text-sm font-bold text-cyan-300">${totAssets.toFixed(1)}M</span>
</div>
<div className="bg-slate-900/90 border border-slate-800 p-2.5 rounded-lg space-y-0.5">
<span className="text-[10px] text-slate-400 block font-sans">LIABILITIES</span>
<span className="text-sm font-bold text-slate-300">${totLiab.toFixed(1)}M</span>
</div>
<div className="bg-slate-900/90 border border-slate-800 p-2.5 rounded-lg space-y-0.5">
<span className="text-[10px] text-slate-400 block font-sans">NET CAPITAL</span>
<span className="text-sm font-bold text-emerald-400">${netEquity.toFixed(1)}M</span>
</div>
<div className="bg-slate-900/90 border border-slate-800 p-2.5 rounded-lg space-y-0.5">
<span className="text-[10px] text-slate-400 block font-sans">ADVANCES</span>
<span className="text-sm font-bold text-amber-300">${advances.toFixed(1)}M</span>
</div>
</div>
{/* Line Item Snapshot List */}
<div className="space-y-1.5 text-xs bg-slate-900/60 p-3 rounded-lg border border-slate-800/80">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-1">
Balance Sheet Portfolio Breakdown
</span>
<div className="flex justify-between items-center text-slate-300">
<span className="truncate">Balances with Banks:</span>
<span className="font-mono font-bold text-white">${balancesWithBanks.toFixed(1)}M</span>
</div>
<div className="flex justify-between items-center text-slate-300">
<span className="truncate">Central Bank Reserve:</span>
<span className="font-mono font-bold text-white">${centralBank.toFixed(1)}M</span>
</div>
<div className="flex justify-between items-center text-slate-300">
<span className="truncate">Investments Portfolio:</span>
<span className="font-mono font-bold text-white">${investments.toFixed(1)}M</span>
</div>
<div className="flex justify-between items-center text-slate-300">
<span className="truncate">Customer Deposits:</span>
<span className="font-mono font-bold text-white">${customerDeposits.toFixed(1)}M</span>
</div>
</div>
{/* Pledging / Variance Alert Badge if applicable */}
{(fullPledged > 0 || partialPledged > 0 || varianceCommentsCount > 0) && (
<div className="p-2.5 rounded-lg bg-amber-950/40 border border-amber-500/40 text-amber-200 text-[11px] space-y-1">
<div className="font-bold flex items-center gap-1.5 text-amber-300">
<AlertTriangle className="w-3.5 h-3.5 text-amber-400 shrink-0" />
Statutory / Variance Notice:
</div>
{fullPledged > 0 && <div> Full Pledged Assets: ${fullPledged.toFixed(1)}M</div>}
{partialPledged > 0 && <div> Partial Pledged Assets: ${partialPledged.toFixed(1)}M</div>}
{varianceCommentsCount > 0 && <div> Active Variances (&gt;$2M): {varianceCommentsCount} item(s) annotated</div>}
</div>
)}
{/* Reconciled Balance Status Banner */}
<div className="flex items-center justify-between pt-1 border-t border-slate-800 text-xs">
<span className="text-slate-400 flex items-center gap-1 font-mono">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
Assets = Liab & Capital
</span>
<span className="text-emerald-400 font-mono font-bold">Diff $0.00M</span>
</div>
</div>
</div>
</div>
);
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,948 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
User,
BranchId,
BRANCHES_LIST,
PlacementRecord,
BorrowingRecord,
InvestmentSecurity,
} from '../types';
import {
Calendar,
Building2,
RefreshCw,
Search,
Filter,
Layers,
BarChart3,
CheckCircle2,
AlertTriangle,
FileSpreadsheet,
Save,
Info,
X,
ChevronRight,
TrendingDown,
TrendingUp,
Download
} from 'lucide-react';
interface LiquidityGapViewProps {
currentUser: User;
activePeriod: string;
}
// Bucket Definition
interface BucketDef {
id: string;
label: string;
subLabel?: string;
minDays?: number;
maxDays?: number;
targetDateStr?: string; // For exact date matching
}
export const LiquidityGapView: React.FC<LiquidityGapViewProps> = ({
currentUser,
activePeriod,
}) => {
const isAdmin = currentUser.role === 'admin';
const initialBranch = currentUser.branchId || 'all';
const [selectedBranchId, setSelectedBranchId] = useState<string>(initialBranch);
const [horizonMode, setHorizonMode] = useState<'weekly' | 'regulatory'>('weekly');
const [placements, setPlacements] = useState<PlacementRecord[]>([]);
const [borrowings, setBorrowings] = useState<BorrowingRecord[]>([]);
const [investments, setInvestments] = useState<InvestmentSecurity[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState<boolean>(false);
const [saveMessage, setSaveMessage] = useState<string | null>(null);
// Custom manual inputs for line items not auto-derived from deals
const [manualInputs, setManualInputs] = useState<Record<string, Record<string, number>>>({
depositOutflows: {},
otherOutflows: {},
advancesInflows: {},
tradeAssetsInflows: {},
otherInflows: {},
});
// Modal / Cell Drilldown State
const [inspectedCell, setInspectedCell] = useState<{
rowLabel: string;
bucketLabel: string;
items: { title: string; category: string; amountUsdMn: number; maturityDate: string; rate?: number }[];
} | null>(null);
useEffect(() => {
fetchData();
}, [activePeriod, selectedBranchId]);
const fetchData = async () => {
setLoading(true);
try {
const branchQuery = selectedBranchId !== 'all' ? `&branchId=${selectedBranchId}` : '';
const [plcRes, borRes, invRes, gapRes] = await Promise.all([
fetch(`/api/placements?period=${encodeURIComponent(activePeriod)}${branchQuery}`),
fetch(`/api/borrowings?period=${encodeURIComponent(activePeriod)}${branchQuery}`),
fetch(`/api/investments?branchId=${selectedBranchId !== 'all' ? selectedBranchId : ''}`),
fetch(`/api/liquidity-gap?period=${encodeURIComponent(activePeriod)}&branchId=${selectedBranchId}`),
]);
if (plcRes.ok) {
const data = await plcRes.json();
setPlacements(data.placements || []);
}
if (borRes.ok) {
const data = await borRes.json();
setBorrowings(data.borrowings || []);
}
if (invRes.ok) {
const data = await invRes.json();
setInvestments(data.investments || []);
}
if (gapRes.ok) {
const data = await gapRes.json();
if (data.overrides) {
setManualInputs(data.overrides);
}
}
} catch (err) {
console.error('Failed to fetch liquidity gap data:', err);
} finally {
setLoading(false);
}
};
const handleSaveOverrides = async () => {
setSaving(true);
setSaveMessage(null);
try {
const res = await fetch('/api/liquidity-gap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
period: activePeriod,
branchId: selectedBranchId,
overrides: manualInputs,
}),
});
if (res.ok) {
setSaveMessage('Manual liquidity gap projections saved successfully!');
setTimeout(() => setSaveMessage(null), 3000);
}
} catch (err) {
console.error('Failed to save gap overrides:', err);
} finally {
setSaving(false);
}
};
// Helper date generator for Weekly Horizon Columns (13 consecutive weeks)
const buckets: BucketDef[] = useMemo(() => {
if (horizonMode === 'regulatory') {
return [
{ id: 'b-7d', label: 'Up to 7 Days', minDays: 0, maxDays: 7 },
{ id: 'b-14d', label: '8 to 14 Days', minDays: 8, maxDays: 14 },
{ id: 'b-30d', label: '15 to 30 Days', minDays: 15, maxDays: 30 },
{ id: 'b-3m', label: '1 to 3 Months', minDays: 31, maxDays: 90 },
{ id: 'b-6m', label: '3 to 6 Months', minDays: 91, maxDays: 180 },
{ id: 'b-12m', label: '6 to 12 Months', minDays: 181, maxDays: 365 },
{ id: 'b-3y', label: '1 to 3 Years', minDays: 366, maxDays: 1095 },
{ id: 'b-over3y', label: 'Over 3 Years', minDays: 1096, maxDays: 99999 },
];
} else {
// Weekly Horizon starting from reporting date or current period
// Build 13 weekly buckets
const baseDate = new Date();
const weeklyBuckets: BucketDef[] = [];
for (let i = 0; i < 13; i++) {
const target = new Date(baseDate.getTime() + i * 7 * 24 * 3600 * 1000);
const day = String(target.getDate()).padStart(2, '0');
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = monthNames[target.getMonth()];
const year = String(target.getFullYear()).slice(-2);
const dateStr = `${day}/${month}/${year}`;
weeklyBuckets.push({
id: `w-${i}`,
label: dateStr,
minDays: i === 0 ? 0 : i * 7 - 3,
maxDays: i === 12 ? 99999 : (i + 1) * 7 + 3,
});
}
return weeklyBuckets;
}
}, [horizonMode, activePeriod]);
// Helper to determine bucket index for a given date
const getBucketIndex = (dateStr: string): number => {
if (!dateStr) return 0;
const maturity = new Date(dateStr);
if (isNaN(maturity.getTime())) return 0;
const today = new Date();
const diffTime = maturity.getTime() - today.getTime();
const diffDays = Math.max(0, Math.ceil(diffTime / (1000 * 3600 * 24)));
for (let i = 0; i < buckets.length; i++) {
const b = buckets[i];
if (b.minDays !== undefined && b.maxDays !== undefined) {
if (diffDays >= b.minDays && diffDays <= b.maxDays) {
return i;
}
}
}
return buckets.length - 1; // Default to last bucket
};
// Build matrix values for all Inflows and Outflows
const matrix = useMemo(() => {
const numBuckets = buckets.length;
// Outflow rows
const depositOutflows = new Array(numBuckets).fill(0);
const otherBankBorrowings = new Array(numBuckets).fill(0);
const hotBorrowings = new Array(numBuckets).fill(0);
const branchBorrowings = new Array(numBuckets).fill(0);
const otherOutflows = new Array(numBuckets).fill(0);
// Inflow rows
const otherBankPlacements = new Array(numBuckets).fill(0);
const hotPlacements = new Array(numBuckets).fill(0);
const branchPlacements = new Array(numBuckets).fill(0);
const maturingInvestments = new Array(numBuckets).fill(0);
const advances = new Array(numBuckets).fill(0);
const tradeAssets = new Array(numBuckets).fill(0);
const otherInflows = new Array(numBuckets).fill(0);
// Detail items storage for drilldown
const drilldownStore: Record<string, any[]> = {};
const addToDrilldown = (rowKey: string, bucketIdx: number, item: any) => {
const key = `${rowKey}_${bucketIdx}`;
if (!drilldownStore[key]) drilldownStore[key] = [];
drilldownStore[key].push(item);
};
// 1. Process Borrowings
borrowings.forEach((b) => {
const bIdx = getBucketIndex(b.maturityDate || b.borrowingDate);
const amt = b.equivalentUsdMn || 0;
if (b.type === 'Inter Bank') {
if (b.counterpartyName.toLowerCase().includes('head office') || b.counterpartyName.toLowerCase().includes('treasury')) {
hotBorrowings[bIdx] += amt;
addToDrilldown('hotBorrowings', bIdx, {
title: b.counterpartyName,
category: 'HOT Borrowing',
amountUsdMn: amt,
maturityDate: b.maturityDate,
rate: b.ratePercent,
});
} else {
otherBankBorrowings[bIdx] += amt;
addToDrilldown('otherBankBorrowings', bIdx, {
title: b.counterpartyName,
category: 'Other Bank/FI Borrowing',
amountUsdMn: amt,
maturityDate: b.maturityDate,
rate: b.ratePercent,
});
}
} else {
branchBorrowings[bIdx] += amt;
addToDrilldown('branchBorrowings', bIdx, {
title: `NBP ${b.counterpartyName}`,
category: 'NBP Branch Borrowing',
amountUsdMn: amt,
maturityDate: b.maturityDate,
rate: b.ratePercent,
});
}
});
// 2. Process Placements
placements.forEach((p) => {
const bIdx = getBucketIndex(p.maturityDate || p.placementDate);
const amt = p.equivalentUsdMn || 0;
if (p.type === 'Inter Bank') {
if (p.counterpartyName.toLowerCase().includes('head office') || p.counterpartyName.toLowerCase().includes('treasury')) {
hotPlacements[bIdx] += amt;
addToDrilldown('hotPlacements', bIdx, {
title: p.counterpartyName,
category: 'HOT Placement',
amountUsdMn: amt,
maturityDate: p.maturityDate,
rate: p.ratePercent,
});
} else {
otherBankPlacements[bIdx] += amt;
addToDrilldown('otherBankPlacements', bIdx, {
title: p.counterpartyName,
category: 'Other Bank/FI Placement',
amountUsdMn: amt,
maturityDate: p.maturityDate,
rate: p.ratePercent,
});
}
} else {
branchPlacements[bIdx] += amt;
addToDrilldown('branchPlacements', bIdx, {
title: `NBP ${p.counterpartyName}`,
category: 'NBP Branch Placement',
amountUsdMn: amt,
maturityDate: p.maturityDate,
rate: p.ratePercent,
});
}
});
// 3. Process Investments
investments.forEach((inv) => {
const bIdx = getBucketIndex(inv.maturityDate || inv.purchaseDate);
const amt = inv.amountInvested || inv.faceValue || 0;
maturingInvestments[bIdx] += amt;
addToDrilldown('maturingInvestments', bIdx, {
title: `${inv.securityType} (${inv.isin})`,
category: `Investment [${inv.category}]`,
amountUsdMn: amt,
maturityDate: inv.maturityDate,
rate: inv.couponRate,
});
});
// 4. Populate Manual / Custom Projection Inputs
buckets.forEach((_, idx) => {
depositOutflows[idx] = manualInputs.depositOutflows?.[idx] || 0;
otherOutflows[idx] = manualInputs.otherOutflows?.[idx] || 0;
advances[idx] = manualInputs.advancesInflows?.[idx] || 0;
tradeAssets[idx] = manualInputs.tradeAssetsInflows?.[idx] || 0;
otherInflows[idx] = manualInputs.otherInflows?.[idx] || 0;
});
// 5. Calculate Row Totals, Periodic Gap, and Cumulative Gap
const totalOutflows = new Array(numBuckets).fill(0);
const totalInflows = new Array(numBuckets).fill(0);
const gap = new Array(numBuckets).fill(0);
const cumulativeGap = new Array(numBuckets).fill(0);
let runningGap = 0;
for (let i = 0; i < numBuckets; i++) {
totalOutflows[i] = Number(
(
depositOutflows[i] +
otherBankBorrowings[i] +
hotBorrowings[i] +
branchBorrowings[i] +
otherOutflows[i]
).toFixed(2)
);
totalInflows[i] = Number(
(
otherBankPlacements[i] +
hotPlacements[i] +
branchPlacements[i] +
maturingInvestments[i] +
advances[i] +
tradeAssets[i] +
otherInflows[i]
).toFixed(2)
);
gap[i] = Number((totalInflows[i] - totalOutflows[i]).toFixed(2));
runningGap += gap[i];
cumulativeGap[i] = Number(runningGap.toFixed(2));
}
return {
depositOutflows,
otherBankBorrowings,
hotBorrowings,
branchBorrowings,
otherOutflows,
totalOutflows,
otherBankPlacements,
hotPlacements,
branchPlacements,
maturingInvestments,
advances,
tradeAssets,
otherInflows,
totalInflows,
gap,
cumulativeGap,
drilldownStore,
};
}, [borrowings, placements, investments, buckets, manualInputs]);
// Handle Manual Input Changes
const handleManualInputChange = (rowKey: string, bucketIdx: number, val: number) => {
setManualInputs((prev) => ({
...prev,
[rowKey]: {
...(prev[rowKey] || {}),
[bucketIdx]: val,
},
}));
};
// Handle Cell Click for Detailed Drilldown
const handleCellClick = (rowLabel: string, rowKey: string, bucketIdx: number) => {
const key = `${rowKey}_${bucketIdx}`;
const items = matrix.drilldownStore[key] || [];
setInspectedCell({
rowLabel,
bucketLabel: buckets[bucketIdx]?.label || `Bucket ${bucketIdx + 1}`,
items,
});
};
const selectedBranchName = selectedBranchId === 'all'
? 'CONSOLIDATED OVERSEAS NETWORK'
: BRANCHES_LIST.find((b) => b.id === selectedBranchId)?.name.toUpperCase() + ' BRANCH';
return (
<div className="space-y-4 max-w-[1700px] mx-auto text-slate-100 font-sans">
{/* Top Header Card matching the Liquidity Gap Report screenshot layout */}
<div className="bg-slate-900/90 border border-slate-700/80 rounded-xl p-3 shadow-lg backdrop-blur-md">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-800 pb-3">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 shrink-0">
<FileSpreadsheet className="w-5 h-5 animate-pulse" />
</div>
<div>
<h1 className="text-sm font-extrabold text-white uppercase tracking-wider font-mono flex items-center gap-2">
LIQUIDITY GAP REPORT & MATURITY PROFILE LADDER
</h1>
<p className="text-xs text-slate-400">
Asset-Liability Management (ALM) Structural Mismatch & Liquidity Exposure Engine
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Horizon Mode Toggle */}
<div className="flex items-center bg-slate-950 p-1 rounded-lg border border-slate-700 text-xs font-mono">
<button
onClick={() => setHorizonMode('weekly')}
className={`px-2.5 py-1 rounded text-xs font-bold transition-all cursor-pointer ${
horizonMode === 'weekly'
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Weekly Horizon
</button>
<button
onClick={() => setHorizonMode('regulatory')}
className={`px-2.5 py-1 rounded text-xs font-bold transition-all cursor-pointer ${
horizonMode === 'regulatory'
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Regulatory Buckets
</button>
</div>
{/* Save Custom Inputs */}
<button
onClick={handleSaveOverrides}
disabled={saving}
className="px-3 py-1.5 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white text-xs font-bold font-mono transition-all flex items-center gap-1.5 shadow-md cursor-pointer"
>
<Save className="w-3.5 h-3.5" />
<span>{saving ? 'Saving...' : 'Save Projections'}</span>
</button>
{/* Refresh */}
<button
onClick={fetchData}
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-600 transition-all cursor-pointer"
title="Refresh Engine"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin text-cyan-400' : ''}`} />
</button>
</div>
</div>
{/* Reporting Metadata Header Grid (matching screenshot) */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-3 font-mono text-xs">
<div className="bg-slate-950/80 p-2 rounded-lg border border-slate-800 flex items-center justify-between">
<span className="text-slate-400 uppercase text-[11px] font-bold">Country / Branch Name:</span>
<select
value={selectedBranchId}
onChange={(e) => setSelectedBranchId(e.target.value)}
className="bg-slate-900 text-cyan-300 px-2 py-0.5 rounded border border-slate-700 text-xs font-bold focus:outline-none"
>
<option value="all" className="bg-slate-900 text-cyan-300">
ALL OVERSEAS BRANCHES (CONSOLIDATED)
</option>
{BRANCHES_LIST.map((b) => (
<option key={b.id} value={b.id} className="bg-slate-900 text-slate-200">
{b.name.toUpperCase()} ({b.code})
</option>
))}
</select>
</div>
<div className="bg-slate-950/80 p-2 rounded-lg border border-slate-800 flex items-center justify-between">
<span className="text-slate-400 uppercase text-[11px] font-bold">Dated:</span>
<span className="text-amber-300 font-bold">{activePeriod}</span>
</div>
<div className="bg-slate-950/80 p-2 rounded-lg border border-slate-800 flex items-center justify-between">
<span className="text-slate-400 uppercase text-[11px] font-bold">Currency Unit:</span>
<span className="text-emerald-300 font-bold bg-emerald-950/80 px-2 py-0.5 rounded border border-emerald-800/80">
USD IN MILLION
</span>
</div>
</div>
</div>
{saveMessage && (
<div className="p-2.5 rounded-lg bg-emerald-950/80 border border-emerald-500/50 text-emerald-200 text-xs flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
<span>{saveMessage}</span>
</div>
)}
{/* Main Liquidity Gap Matrix Table (Exact visual replica of screenshot) */}
<div className="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden shadow-xl">
<div className="overflow-x-auto">
<table className="w-full text-left font-mono border-collapse high-density-table">
{/* Table Header Row: Liquidity Gap Report Title + Date Buckets */}
<thead>
<tr className="bg-slate-950 text-slate-200 border-b-2 border-slate-700 font-bold text-xs uppercase">
<th className="p-3 border-r border-slate-800 w-72 min-w-[280px] bg-slate-950 text-cyan-400 font-mono text-sm tracking-wide">
Liquidity Gap Report
</th>
{buckets.map((b) => (
<th
key={b.id}
className="p-2 border-r border-slate-800 text-center min-w-[105px] text-[11px] text-slate-300 font-mono"
>
<div>{b.label}</div>
{b.subLabel && <div className="text-[9px] text-slate-500 font-normal">{b.subLabel}</div>}
</th>
))}
</tr>
</thead>
<tbody>
{/* SECTION 1: OUTFLOWS (-) [RED HEADING] */}
<tr className="bg-rose-950/40 text-rose-300 font-extrabold uppercase border-y border-rose-900/60 text-xs">
<td colSpan={buckets.length + 1} className="p-2 tracking-wider flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-rose-500 inline-block"></span>
OUTFLOWS (-)
</td>
</tr>
{/* Row 1.1: Customer Deposit outflows (-) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-rose-400 font-semibold pl-4">
Customer Deposit outflows (-)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-1 border-r border-slate-800/80 text-right">
<input
type="number"
step="0.01"
value={matrix.depositOutflows[idx] || ''}
onChange={(e) => handleManualInputChange('depositOutflows', idx, Number(e.target.value))}
placeholder="-"
className="w-full bg-slate-950/60 text-rose-300 text-right px-1 py-0.5 rounded border border-slate-800 focus:border-rose-500 text-[11px] font-mono focus:outline-none"
/>
</td>
))}
</tr>
{/* Row 1.2: Other Bank/FI Borrowing (-) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-rose-400 font-semibold pl-4">
Other Bank/FI Borrowing (-)
</td>
{buckets.map((_, idx) => {
const val = matrix.otherBankBorrowings[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('Other Bank/FI Borrowing', 'otherBankBorrowings', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-rose-300 font-bold hover:underline cursor-pointer bg-rose-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 1.3: HOT Borrowing (-) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-rose-400 font-semibold pl-4">
HOT Borrowing (-)
</td>
{buckets.map((_, idx) => {
const val = matrix.hotBorrowings[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('HOT Borrowing', 'hotBorrowings', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-rose-300 font-bold hover:underline cursor-pointer bg-rose-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 1.4: NBP Branch Borrowing (-) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-rose-400 font-semibold pl-4">
NBP Branch Borrowing (-)
</td>
{buckets.map((_, idx) => {
const val = matrix.branchBorrowings[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('NBP Branch Borrowing', 'branchBorrowings', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-rose-300 font-bold hover:underline cursor-pointer bg-rose-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 1.5: Other Expected Outflows (-) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-rose-400 font-semibold pl-4">
Other Expected Outflows (-)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-1 border-r border-slate-800/80 text-right">
<input
type="number"
step="0.01"
value={matrix.otherOutflows[idx] || ''}
onChange={(e) => handleManualInputChange('otherOutflows', idx, Number(e.target.value))}
placeholder="-"
className="w-full bg-slate-950/60 text-rose-300 text-right px-1 py-0.5 rounded border border-slate-800 focus:border-rose-500 text-[11px] font-mono focus:outline-none"
/>
</td>
))}
</tr>
{/* TOTAL OUTFLOWS ROW */}
<tr className="bg-rose-950/60 text-rose-200 font-extrabold text-xs border-b-2 border-slate-700 uppercase">
<td className="p-2.5 border-r border-slate-800 tracking-wider">
TOTAL OUTFLOWS (-)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-2.5 border-r border-slate-800 text-right text-rose-300 font-mono font-extrabold text-xs">
{matrix.totalOutflows[idx] > 0 ? matrix.totalOutflows[idx].toFixed(2) : '-'}
</td>
))}
</tr>
{/* SECTION 2: INFLOWS (+) [GREEN HEADING] */}
<tr className="bg-emerald-950/40 text-emerald-300 font-extrabold uppercase border-y border-emerald-900/60 text-xs">
<td colSpan={buckets.length + 1} className="p-2 tracking-wider flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 inline-block"></span>
INFLOWS (+)
</td>
</tr>
{/* Row 2.1: Other Bank/FI/Placement/Lending (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
Other Bank/FI/Placement/Lending (+)
</td>
{buckets.map((_, idx) => {
const val = matrix.otherBankPlacements[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('Other Bank/FI Placement', 'otherBankPlacements', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-emerald-300 font-bold hover:underline cursor-pointer bg-emerald-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 2.2: HOT Placement/Lending (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
HOT Placement/Lending (+)
</td>
{buckets.map((_, idx) => {
const val = matrix.hotPlacements[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('HOT Placement', 'hotPlacements', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-emerald-300 font-bold hover:underline cursor-pointer bg-emerald-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 2.3: NBP Branch Placement/Lending (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
NBP Branch Placement/Lending (+)
</td>
{buckets.map((_, idx) => {
const val = matrix.branchPlacements[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('NBP Branch Placement', 'branchPlacements', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-emerald-300 font-bold hover:underline cursor-pointer bg-emerald-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 2.4: Maturing Investments (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
Maturing Investments (+)
</td>
{buckets.map((_, idx) => {
const val = matrix.maturingInvestments[idx];
return (
<td
key={idx}
onClick={() => val > 0 && handleCellClick('Maturing Investments', 'maturingInvestments', idx)}
className={`p-2 border-r border-slate-800/80 text-right font-mono text-[11px] ${
val > 0 ? 'text-emerald-300 font-bold hover:underline cursor-pointer bg-emerald-950/20' : 'text-slate-600'
}`}
>
{val > 0 ? val.toFixed(2) : '-'}
</td>
);
})}
</tr>
{/* Row 2.5: Advances (+) (Excluding Trade Assets) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
Advances (+) (Excluding Trade Assets)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-1 border-r border-slate-800/80 text-right">
<input
type="number"
step="0.01"
value={matrix.advances[idx] || ''}
onChange={(e) => handleManualInputChange('advancesInflows', idx, Number(e.target.value))}
placeholder="-"
className="w-full bg-slate-950/60 text-emerald-300 text-right px-1 py-0.5 rounded border border-slate-800 focus:border-emerald-500 text-[11px] font-mono focus:outline-none"
/>
</td>
))}
</tr>
{/* Row 2.6: Trade Assets (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
Trade Assets (+)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-1 border-r border-slate-800/80 text-right">
<input
type="number"
step="0.01"
value={matrix.tradeAssets[idx] || ''}
onChange={(e) => handleManualInputChange('tradeAssetsInflows', idx, Number(e.target.value))}
placeholder="-"
className="w-full bg-slate-950/60 text-emerald-300 text-right px-1 py-0.5 rounded border border-slate-800 focus:border-emerald-500 text-[11px] font-mono focus:outline-none"
/>
</td>
))}
</tr>
{/* Row 2.7: Other Expected Inflows (+) */}
<tr className="hover:bg-slate-800/60 transition-colors text-xs border-b border-slate-800/60">
<td className="p-2 border-r border-slate-800/80 text-emerald-400 font-semibold pl-4">
Other Expected Inflows (+)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-1 border-r border-slate-800/80 text-right">
<input
type="number"
step="0.01"
value={matrix.otherInflows[idx] || ''}
onChange={(e) => handleManualInputChange('otherInflows', idx, Number(e.target.value))}
placeholder="-"
className="w-full bg-slate-950/60 text-emerald-300 text-right px-1 py-0.5 rounded border border-slate-800 focus:border-emerald-500 text-[11px] font-mono focus:outline-none"
/>
</td>
))}
</tr>
{/* TOTAL INFLOWS ROW */}
<tr className="bg-emerald-950/60 text-emerald-200 font-extrabold text-xs border-b-2 border-slate-700 uppercase">
<td className="p-2.5 border-r border-slate-800 tracking-wider">
TOTAL INFLOWS (+)
</td>
{buckets.map((_, idx) => (
<td key={idx} className="p-2.5 border-r border-slate-800 text-right text-emerald-300 font-mono font-extrabold text-xs">
{matrix.totalInflows[idx] > 0 ? matrix.totalInflows[idx].toFixed(2) : '-'}
</td>
))}
</tr>
{/* SECTION 3: GAP ROW (NET PERIODIC MISMATCH) */}
<tr className="bg-cyan-950/80 text-cyan-200 font-extrabold text-xs border-b border-slate-700 uppercase">
<td className="p-2.5 border-r border-slate-800 tracking-wider flex items-center gap-2">
<span className="px-1.5 py-0.5 bg-cyan-500/20 text-cyan-300 rounded text-[10px]">NET</span>
GAP
</td>
{buckets.map((_, idx) => {
const gVal = matrix.gap[idx];
return (
<td
key={idx}
className={`p-2.5 border-r border-slate-800 text-right font-mono font-extrabold text-xs ${
gVal > 0 ? 'text-emerald-400' : gVal < 0 ? 'text-rose-400' : 'text-slate-500'
}`}
>
{gVal !== 0 ? (gVal > 0 ? `+${gVal.toFixed(2)}` : gVal.toFixed(2)) : '-'}
</td>
);
})}
</tr>
{/* SECTION 4: CUMULATIVE GAP ROW (BLACK/GOLD HEADER LIKE SCREENSHOT) */}
<tr className="bg-black text-amber-300 font-black text-xs border-b-2 border-slate-700 uppercase">
<td className="p-3 border-r border-slate-800 tracking-wider">
CUMULATIVE GAP
</td>
{buckets.map((_, idx) => {
const cVal = matrix.cumulativeGap[idx];
return (
<td
key={idx}
className={`p-3 border-r border-slate-800 text-right font-mono font-black text-xs ${
cVal > 0 ? 'text-emerald-300' : cVal < 0 ? 'text-rose-400' : 'text-amber-400'
}`}
>
{cVal !== 0 ? (cVal > 0 ? `+${cVal.toFixed(2)}` : cVal.toFixed(2)) : '-'}
</td>
);
})}
</tr>
</tbody>
</table>
</div>
{/* Footer Legend matching screenshot */}
<div className="bg-slate-950 p-3 border-t border-slate-800 text-[11px] text-slate-400 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-4">
<span className="text-rose-400 font-semibold">HOT = Head Office Treasury</span>
<span className="text-cyan-300 font-semibold">NBP = National Bank Network</span>
<span>All figures auto-derived from Placements, Borrowings & Investments Portfolios.</span>
</div>
<span className="font-mono text-slate-500">Unit: Million USD</span>
</div>
</div>
{/* Drilldown Modal / Drawer when a non-zero deal cell is clicked */}
{inspectedCell && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-slate-900 border border-slate-700 rounded-xl max-w-2xl w-full p-5 shadow-2xl space-y-4">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<div>
<h3 className="text-sm font-bold text-white uppercase font-mono flex items-center gap-2">
<Layers className="w-4 h-4 text-cyan-400" />
Deals Breakdown: {inspectedCell.rowLabel}
</h3>
<p className="text-xs text-slate-400 mt-0.5">
Maturity Bucket: <span className="text-amber-300 font-mono font-bold">{inspectedCell.bucketLabel}</span>
</p>
</div>
<button
onClick={() => setInspectedCell(null)}
className="p-1 rounded bg-slate-800 text-slate-400 hover:text-white cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="max-h-80 overflow-y-auto space-y-2 font-mono text-xs">
{inspectedCell.items.length === 0 ? (
<div className="text-center py-6 text-slate-500">
No individual deal records mapped to this specific cell.
</div>
) : (
inspectedCell.items.map((item, index) => (
<div
key={index}
className="p-3 bg-slate-950 rounded-lg border border-slate-800 flex items-center justify-between gap-3 hover:border-slate-700"
>
<div>
<div className="font-bold text-slate-100">{item.title}</div>
<div className="text-[10px] text-slate-400 flex items-center gap-2 mt-0.5">
<span className="bg-slate-800 px-1.5 py-0.2 rounded text-cyan-300">{item.category}</span>
<span>Maturity: {item.maturityDate}</span>
{item.rate !== undefined && <span>Rate: {item.rate}%</span>}
</div>
</div>
<div className="text-right shrink-0">
<div className="text-sm font-extrabold text-cyan-300">
USD {item.amountUsdMn.toFixed(2)} Mn
</div>
</div>
</div>
))
)}
</div>
<div className="flex justify-between items-center border-t border-slate-800 pt-3 text-xs">
<span className="text-slate-400 font-mono">
Total Bucket Items: <span className="text-white font-bold">{inspectedCell.items.length}</span>
</span>
<button
onClick={() => setInspectedCell(null)}
className="px-4 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 font-bold rounded-lg text-xs cursor-pointer"
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
};

626
src/components/Navbar.tsx Normal file
View file

@ -0,0 +1,626 @@
import React, { useState, useEffect } from 'react';
import {
Building2,
Globe2,
ShieldCheck,
Mail,
BarChart3,
CheckCircle2,
FileSpreadsheet,
UserCheck,
Calendar,
AlertTriangle,
RotateCcw,
Zap,
Send,
Users,
ChevronDown,
Clock,
Activity,
Menu,
Briefcase,
ArrowLeftRight,
X,
PanelLeftClose,
PanelLeftOpen,
ChevronLeft,
ChevronRight,
Key
} from 'lucide-react';
import { User, BranchId, BRANCHES_LIST } from '../types';
interface NavigationProps {
currentUser: User;
setCurrentUser: (user: User) => void;
activeTab: 'consolidated' | 'branch_input' | 'placements_borrowings' | 'investments' | 'liquidity_gap' | 'autochecks' | 'analytics' | 'admin';
setActiveTab: (tab: 'consolidated' | 'branch_input' | 'placements_borrowings' | 'investments' | 'liquidity_gap' | 'autochecks' | 'analytics' | 'admin') => void;
activePeriod: string;
setActivePeriod: (period: string) => void;
periodsList: string[];
allUsers: User[];
onResetSeed: () => void;
onOpenAuthModal?: () => void;
}
// Left Sidebar Component for Desktop & Tablet
export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActivePeriod' | 'periodsList' | 'onResetSeed'>> = ({
currentUser,
setCurrentUser,
activeTab,
setActiveTab,
allUsers,
onOpenAuthModal
}) => {
const [showUserMenu, setShowUserMenu] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const isAdmin = currentUser.role === 'admin';
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
return (
<aside
className={`transition-all duration-300 ease-in-out ${
isCollapsed ? 'w-16' : 'w-60 lg:w-64'
} bg-slate-900/95 backdrop-blur-2xl border-r border-cyan-500/40 flex flex-col shrink-0 select-none z-30 hidden md:flex hud-corner shadow-[4px_0_25px_rgba(0,0,0,0.6)] h-full overflow-x-hidden`}
>
{/* Brand Header with Holographic Glow */}
<div className="p-3.5 border-b border-cyan-500/20 flex items-center justify-between bg-cyan-950/20 shrink-0">
{!isCollapsed && (
<div className="flex items-center gap-2.5 overflow-hidden">
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.8)] text-base shrink-0">
B
</div>
<div className="overflow-hidden whitespace-nowrap">
<h1 className="text-sm font-bold tracking-tight text-white flex items-center gap-0.5">
EQUITY<span className="text-cyan-400 font-extrabold glow-text-cyan">CORE</span>
</h1>
<p className="text-[9px] text-cyan-300/70 font-mono tracking-widest uppercase">QUANTUM BALANCE SHEET</p>
</div>
</div>
)}
{isCollapsed && (
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.8)] text-base shrink-0 mx-auto">
B
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className={`p-1.5 rounded-lg bg-slate-800/80 hover:bg-slate-700 text-cyan-300 hover:text-white border border-cyan-500/30 transition-all cursor-pointer ${
isCollapsed ? 'mt-2 mx-auto block' : ''
}`}
title={isCollapsed ? 'Expand Navigation Sidebar' : 'Collapse Navigation Sidebar'}
>
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
{/* Navigation Section */}
<nav className="flex-1 p-2 space-y-1.5 overflow-y-auto overflow-x-hidden">
{!isCollapsed ? (
<div className="text-[9px] uppercase tracking-widest text-cyan-400/80 font-bold mb-2 px-3 pt-1 font-mono flex items-center justify-between">
<span>Principal Matrix</span>
<span className="w-1.5 h-1.5 bg-cyan-400 rounded-full animate-ping"></span>
</div>
) : (
<div className="my-2 border-b border-cyan-500/20"></div>
)}
{/* Consolidated View */}
<button
onClick={() => setActiveTab('consolidated')}
disabled={!isAdmin}
title="Consolidated Matrix"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'consolidated'
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-400/60 shadow-[0_0_15px_rgba(6,182,212,0.25)] glow-text-cyan'
: !isAdmin
? 'text-slate-600 cursor-not-allowed opacity-40'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-cyan-200 hover:border hover:border-cyan-500/30'
}`}
>
<div className="flex items-center gap-2.5">
<FileSpreadsheet className={`w-4 h-4 shrink-0 ${activeTab === 'consolidated' ? 'text-cyan-400' : 'text-slate-400'}`} />
{!isCollapsed && <span>Consolidated Matrix</span>}
</div>
{!isCollapsed && !isAdmin && (
<span className="text-[9px] bg-slate-900 text-slate-500 px-1 py-0.5 rounded border border-slate-800 font-mono">Restricted</span>
)}
</button>
{/* Branch Input */}
<button
onClick={() => setActiveTab('branch_input')}
title={isAdmin ? 'Branch Entry Portal' : `${currentBranch?.code || 'Branch'} Entry`}
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'branch_input'
? 'bg-amber-500/20 text-amber-300 border border-amber-500/60 shadow-[0_0_15px_rgba(245,158,11,0.25)]'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-amber-200'
}`}
>
<div className="flex items-center gap-2.5">
<Building2 className={`w-4 h-4 shrink-0 ${activeTab === 'branch_input' ? 'text-amber-400' : 'text-slate-400'}`} />
{!isCollapsed && (
<span className="truncate">
{isAdmin ? 'Branch Entry Portal' : `${currentBranch?.code || 'Branch'} Entry`}
</span>
)}
</div>
{!isCollapsed && (
<span className="text-[9px] bg-amber-500/20 text-amber-300 px-1.5 py-0.5 rounded border border-amber-500/40 font-mono">INPUT</span>
)}
</button>
{/* Placements & Borrowings Schedule */}
<button
onClick={() => setActiveTab('placements_borrowings')}
title="Placements & Borrowings Schedule"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'placements_borrowings'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/60 shadow-[0_0_15px_rgba(16,185,129,0.25)]'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-emerald-200'
}`}
>
<div className="flex items-center gap-2.5">
<ArrowLeftRight className={`w-4 h-4 shrink-0 ${activeTab === 'placements_borrowings' ? 'text-emerald-400' : 'text-slate-400'}`} />
{!isCollapsed && <span className="truncate">Placements & Borrowings</span>}
</div>
{!isCollapsed && (
<span className="text-[9px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/40 font-mono">SCHEDULE</span>
)}
</button>
{/* Investments Portfolio */}
<button
onClick={() => setActiveTab('investments')}
title="Investments Portfolio"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'investments'
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-400/60 shadow-[0_0_15px_rgba(6,182,212,0.25)]'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-cyan-200'
}`}
>
<div className="flex items-center gap-2.5">
<Briefcase className={`w-4 h-4 shrink-0 ${activeTab === 'investments' ? 'text-cyan-400' : 'text-slate-400'}`} />
{!isCollapsed && <span className="truncate">Investments Portfolio</span>}
</div>
{!isCollapsed && (
<span className="text-[9px] bg-cyan-500/20 text-cyan-300 px-1.5 py-0.5 rounded border border-cyan-500/40 font-mono">PORTFOLIO</span>
)}
</button>
{/* Liquidity Gap & ALM */}
<button
onClick={() => setActiveTab('liquidity_gap')}
title="Liquidity Gap & ALM"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'liquidity_gap'
? 'bg-amber-500/20 text-amber-300 border border-amber-400/60 shadow-[0_0_15px_rgba(245,158,11,0.25)]'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-amber-200'
}`}
>
<div className="flex items-center gap-2.5">
<FileSpreadsheet className={`w-4 h-4 shrink-0 ${activeTab === 'liquidity_gap' ? 'text-amber-400' : 'text-slate-400'}`} />
{!isCollapsed && <span className="truncate">Liquidity Gap & ALM</span>}
</div>
{!isCollapsed && (
<span className="text-[9px] bg-amber-500/20 text-amber-300 px-1.5 py-0.5 rounded border border-amber-500/40 font-mono">MATURITY</span>
)}
</button>
{/* Auto Checks */}
<button
onClick={() => setActiveTab('autochecks')}
disabled={!isAdmin}
title="Auto-Checks Engine"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'autochecks'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/60 shadow-[0_0_15px_rgba(16,185,129,0.25)] glow-text-emerald'
: !isAdmin
? 'text-slate-600 cursor-not-allowed opacity-40'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-emerald-200'
}`}
>
<div className="flex items-center gap-2.5">
<Zap className={`w-4 h-4 shrink-0 ${activeTab === 'autochecks' ? 'text-emerald-400' : 'text-slate-400'}`} />
{!isCollapsed && <span>Auto-Checks Engine</span>}
</div>
{!isCollapsed && (
<span className="text-[9px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/40 font-mono">100% RECON</span>
)}
</button>
{/* Analytics Visuals */}
<button
onClick={() => setActiveTab('analytics')}
disabled={!isAdmin}
title="Executive Visuals"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'analytics'
? 'bg-pink-500/20 text-pink-300 border border-pink-500/60 shadow-[0_0_15px_rgba(236,72,153,0.25)] glow-text-magenta'
: !isAdmin
? 'text-slate-600 cursor-not-allowed opacity-40'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-pink-200'
}`}
>
<div className="flex items-center gap-2.5">
<BarChart3 className={`w-4 h-4 shrink-0 ${activeTab === 'analytics' ? 'text-pink-400' : 'text-slate-400'}`} />
{!isCollapsed && <span>Executive Visuals</span>}
</div>
</button>
{/* Admin & SMTP */}
{isAdmin && (
<button
onClick={() => setActiveTab('admin')}
title="Admin & FX Config"
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2.5' : 'justify-between px-3 py-2.5'
} rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'admin'
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/60 shadow-[0_0_15px_rgba(99,102,241,0.25)]'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-indigo-200'
}`}
>
<div className="flex items-center gap-2.5">
<Mail className={`w-4 h-4 shrink-0 ${activeTab === 'admin' ? 'text-indigo-400' : 'text-slate-400'}`} />
{!isCollapsed && <span>Admin & FX Config</span>}
</div>
</button>
)}
{/* Telemetry */}
{!isCollapsed ? (
<div className="pt-4 border-t border-cyan-500/20 space-y-2">
<div className="text-[9px] uppercase tracking-widest text-cyan-400/80 font-bold px-3 font-mono">
Matrix Telemetry
</div>
<div className="px-3 py-2 flex items-center justify-between text-xs text-slate-300 bg-slate-950/80 rounded-lg border border-cyan-500/20">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_#10b981]"></span>
<span className="text-[11px] font-mono">SMTP Relay</span>
</div>
<span className="text-[10px] font-mono text-emerald-400 font-bold">ONLINE</span>
</div>
<div className="px-3 py-2 flex items-center justify-between text-xs text-slate-300 bg-slate-950/80 rounded-lg border border-cyan-500/20">
<div className="flex items-center gap-2">
<Activity className="w-3.5 h-3.5 text-cyan-400" />
<span className="text-[11px] font-mono">3D WebGL</span>
</div>
<span className="text-[10px] font-mono text-cyan-400 font-bold">READY</span>
</div>
</div>
) : (
<div className="pt-3 border-t border-cyan-500/20 flex flex-col items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400 shadow-[0_0_8px_#10b981]" title="SMTP Relay Online"></span>
<Activity className="w-3.5 h-3.5 text-cyan-400" title="3D WebGL Engine Ready" />
</div>
)}
</nav>
{/* Sidebar Footer User Profile */}
<div className="p-2 border-t border-cyan-500/20 bg-slate-950/90 relative shrink-0">
<button
onClick={() => {
setShowUserMenu(false);
onOpenAuthModal?.();
}}
className={`w-full flex items-center ${
isCollapsed ? 'justify-center p-2' : 'justify-between p-2'
} rounded-xl bg-slate-900/60 border border-cyan-500/30 hover:border-cyan-400/60 transition-all text-left cursor-pointer shadow-sm`}
title={`Active Session: ${currentUser.name} - Click to Authenticate Credentials`}
>
<div className="flex items-center gap-2.5 overflow-hidden">
<div className="w-8 h-8 rounded-full bg-cyan-500/20 border border-cyan-400/60 flex items-center justify-center text-cyan-300 font-bold text-xs shrink-0 shadow-[0_0_10px_rgba(6,182,212,0.4)]">
{currentUser.role === 'admin' ? 'HQ' : currentUser.branchId?.substring(0, 2).toUpperCase()}
</div>
{!isCollapsed && (
<div className="overflow-hidden">
<p className="text-xs font-bold text-white truncate flex items-center gap-1">
{currentUser.name}
{isAdmin && <ShieldCheck className="w-3 h-3 text-cyan-400 shrink-0" />}
</p>
<p className="text-[10px] text-cyan-400 font-mono truncate font-semibold">Authenticate / Switch</p>
</div>
)}
</div>
{!isCollapsed && <Key className="w-3.5 h-3.5 text-amber-400 shrink-0" />}
</button>
</div>
</aside>
);
};
// Top Header Bar Component with Mobile Navigation Drawer
export const HeaderBar: React.FC<NavigationProps> = ({
currentUser,
setCurrentUser,
activeTab,
setActiveTab,
activePeriod,
setActivePeriod,
periodsList,
allUsers,
onResetSeed,
onOpenAuthModal
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const [countdown, setCountdown] = useState({ days: 3, hours: 14, mins: 22 });
useEffect(() => {
const interval = setInterval(() => {
setCountdown((prev) => {
if (prev.mins > 0) return { ...prev, mins: prev.mins - 1 };
return { ...prev, mins: 59, hours: prev.hours > 0 ? prev.hours - 1 : 23 };
});
}, 60000);
return () => clearInterval(interval);
}, []);
const isAdmin = currentUser.role === 'admin';
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
return (
<header className="h-14 border-b border-cyan-500/40 bg-slate-900/95 backdrop-blur-2xl flex items-center justify-between px-4 sm:px-6 shrink-0 z-20 w-full shadow-[0_4px_20px_rgba(0,0,0,0.5)] relative">
<div className="flex items-center gap-3 sm:gap-4">
{/* Mobile Menu Toggle Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-1.5 rounded-lg bg-slate-900 border border-cyan-500/30 text-cyan-400 hover:bg-slate-800 transition-colors"
title="Toggle Navigation Menu"
>
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
{/* Brand Badge */}
<div className="flex items-center gap-2">
<div className="w-7 h-7 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 text-xs shadow-[0_0_10px_#06b6d4]">B</div>
<span className="font-bold text-white text-xs tracking-wider md:hidden">EQUITY<span className="text-cyan-400">CORE</span></span>
</div>
<h2 className="text-xs sm:text-sm font-bold text-white hidden sm:flex items-center gap-2 glow-text-cyan">
{activeTab === 'consolidated' && 'Quantum Consolidation Matrix'}
{activeTab === 'branch_input' && 'Branch Balance Sheet Submission Portal'}
{activeTab === 'placements_borrowings' && 'Inter-Branch & Inter-Bank Placements & Borrowings Ledger'}
{activeTab === 'investments' && 'Investments Portfolio & Mark-to-Market Engine'}
{activeTab === 'autochecks' && 'Automated Inter-Network Reconciliation Scanner'}
{activeTab === 'analytics' && 'Executive Visual Telemetry & Analytics'}
{activeTab === 'admin' && 'Head Office Controller & SMTP Config'}
</h2>
<div className="flex items-center gap-2">
<span className="hidden sm:inline-block px-2 py-0.5 rounded bg-slate-900 text-cyan-300 text-[10px] font-mono border border-cyan-500/30 uppercase">
Period:
</span>
{/* Date Select Dropdown */}
<select
value={activePeriod}
onChange={(e) => setActivePeriod(e.target.value)}
className="bg-slate-900 text-amber-300 text-xs font-mono font-bold rounded-lg px-2 py-1 border border-cyan-500/30 focus:outline-none focus:ring-1 focus:ring-cyan-400 cursor-pointer shadow-inner"
>
{periodsList.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center gap-3">
<div className="hidden lg:flex items-center gap-2 bg-slate-900/80 px-3 py-1 rounded-xl border border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.1)]">
<Clock className="w-3.5 h-3.5 text-amber-400 animate-pulse" />
<div className="text-right">
<span className="text-[9px] text-slate-400 uppercase font-bold tracking-tight block leading-none font-mono">
PERIOD CLOSES IN
</span>
<span className="text-xs font-mono text-amber-300 font-bold uppercase leading-tight">
{String(countdown.days).padStart(2, '0')}d : {String(countdown.hours).padStart(2, '0')}h : {String(countdown.mins).padStart(2, '0')}m
</span>
</div>
</div>
<div className="flex items-center gap-2">
{onOpenAuthModal && (
<button
onClick={onOpenAuthModal}
className="bg-gradient-to-r from-cyan-500/20 via-blue-500/20 to-indigo-500/20 hover:from-cyan-500/30 hover:to-indigo-500/30 text-cyan-300 text-xs px-2.5 py-1.5 rounded-lg font-bold border border-cyan-400/50 transition-all flex items-center gap-1.5 cursor-pointer shadow-[0_0_10px_rgba(6,182,212,0.2)]"
title="Authenticate Branch Credentials or Switch User Persona"
>
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400" />
<span className="hidden sm:inline">Auth Matrix</span>
</button>
)}
<button
onClick={onResetSeed}
className="bg-slate-900 hover:bg-slate-800 text-cyan-300 hover:text-white text-xs px-2.5 py-1.5 rounded-lg font-semibold border border-cyan-500/30 transition-all flex items-center gap-1.5 cursor-pointer shadow-sm hover:shadow-[0_0_15px_rgba(6,182,212,0.3)]"
title="Reset sample data back to baseline"
>
<RotateCcw className="w-3.5 h-3.5 text-cyan-400" />
<span className="hidden sm:inline">Reset Demo</span>
</button>
</div>
</div>
{/* Mobile Drawer Overlay */}
{mobileMenuOpen && (
<div className="md:hidden fixed inset-x-0 top-14 bg-slate-950/95 backdrop-blur-2xl border-b border-cyan-500/30 p-4 z-50 shadow-[0_10px_30px_rgba(0,0,0,0.8)] space-y-3">
<div className="text-[10px] font-mono uppercase tracking-widest text-cyan-400 font-bold flex items-center justify-between pb-1 border-b border-slate-800">
<span>Navigation Matrix</span>
<span className="text-xs text-amber-300">{currentUser.name}</span>
</div>
<div className="grid grid-cols-1 gap-2">
{isAdmin && (
<button
onClick={() => {
setActiveTab('consolidated');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'consolidated' ? 'bg-cyan-500/20 text-cyan-300 border border-cyan-400' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<FileSpreadsheet className="w-4 h-4 text-cyan-400" />
<span>Consolidated Matrix</span>
</div>
</button>
)}
<button
onClick={() => {
setActiveTab('branch_input');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'branch_input' ? 'bg-amber-500/20 text-amber-300 border border-amber-500' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-amber-400" />
<span>{isAdmin ? 'Branch Entry Portal' : `${currentBranch?.code || 'Branch'} Entry`}</span>
</div>
</button>
<button
onClick={() => {
setActiveTab('investments');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'investments' ? 'bg-cyan-500/20 text-cyan-300 border border-cyan-400' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<Briefcase className="w-4 h-4 text-cyan-400" />
<span>Investments Portfolio</span>
</div>
</button>
<button
onClick={() => {
setActiveTab('liquidity_gap');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'liquidity_gap' ? 'bg-amber-500/20 text-amber-300 border border-amber-400' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<FileSpreadsheet className="w-4 h-4 text-amber-400" />
<span>Liquidity Gap & ALM</span>
</div>
</button>
{isAdmin && (
<>
<button
onClick={() => {
setActiveTab('autochecks');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'autochecks' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-400" />
<span>Auto-Checks Engine</span>
</div>
</button>
<button
onClick={() => {
setActiveTab('analytics');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'analytics' ? 'bg-pink-500/20 text-pink-300 border border-pink-500' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-pink-400" />
<span>Executive Visuals</span>
</div>
</button>
<button
onClick={() => {
setActiveTab('admin');
setMobileMenuOpen(false);
}}
className={`flex items-center justify-between p-2.5 rounded-lg text-xs font-semibold ${
activeTab === 'admin' ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500' : 'bg-slate-900 text-slate-300'
}`}
>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-indigo-400" />
<span>Admin & SMTP Config</span>
</div>
</button>
</>
)}
</div>
{/* Quick User Identity Switcher for Mobile */}
<div className="pt-2 border-t border-slate-800">
<button
onClick={() => setShowUserMenu(!showUserMenu)}
className="w-full flex items-center justify-between p-2 bg-slate-900 rounded-lg border border-cyan-500/30 text-xs text-cyan-300"
>
<span>Simulate User: {currentUser.name}</span>
<ChevronDown className="w-4 h-4 text-cyan-400" />
</button>
{showUserMenu && (
<div className="mt-2 p-2 bg-slate-900 rounded-lg border border-cyan-500/40 space-y-1 max-h-48 overflow-y-auto">
{allUsers.filter((u) => u.approved).map((user) => (
<button
key={user.id}
onClick={() => {
setCurrentUser(user);
setShowUserMenu(false);
setMobileMenuOpen(false);
if (user.role === 'branch_user') {
setActiveTab('branch_input');
}
}}
className={`w-full text-left p-1.5 rounded text-xs flex justify-between items-center ${
user.id === currentUser.id ? 'bg-cyan-500/30 text-white font-bold' : 'text-slate-300 hover:bg-slate-800'
}`}
>
<span>{user.name}</span>
<span className="text-[9px] font-mono text-cyan-400">{user.role}</span>
</button>
))}
</div>
)}
</div>
</div>
)}
</header>
);
};
// Backward-compatible export
export const Navbar = HeaderBar;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,859 @@
import { InvestmentSecurity, BranchId } from '../types';
export const DEFAULT_INVESTMENTS_SEED: InvestmentSecurity[] = [
// ==========================================
// BAHRAIN BRANCH PORTFOLIO
// ==========================================
{
id: 'inv-bhr-1',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS2419405274',
securityType: 'PAKISTAN SUKUK-7.95 PCT SUK',
faceValue: 14.00,
amountInvested: 14.00,
couponRate: 7.95,
issueDate: '31-Jan-22',
purchaseDate: '31-Jan-22',
maturityDate: '31-Jan-29',
originalPrice: 100.00,
dtmeDays: 945,
purchaseYield: 7.95,
mtmPrice: 99.99,
mtmYield: 7.95,
mtmPnL: 0,
},
{
id: 'inv-bhr-2',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2028',
faceValue: 10.00,
amountInvested: 10.17,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '13-Jul-21',
maturityDate: '08-Apr-31',
originalPrice: 101.70,
dtmeDays: 1742,
purchaseYield: 7.13,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -59036.73,
},
{
id: 'inv-bhr-3',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS1729875598',
securityType: 'GOP EuroBond 2027',
faceValue: 2.00,
amountInvested: 1.74,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '10-May-18',
maturityDate: '05-Dec-27',
originalPrice: 93.00,
dtmeDays: 522,
purchaseYield: 7.93,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 38224.85,
},
{
id: 'inv-bhr-4',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS1729875598',
securityType: 'GOP EuroBond 2027',
faceValue: 2.00,
amountInvested: 1.75,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '17-Aug-18',
maturityDate: '05-Dec-27',
originalPrice: 90.00,
dtmeDays: 522,
purchaseYield: 8.45,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 51717.60,
},
{
id: 'inv-bhr-5',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS1729875598',
securityType: 'GOP EuroBond 2027',
faceValue: 2.00,
amountInvested: 1.81,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '11-Jun-19',
maturityDate: '05-Dec-27',
originalPrice: 90.50,
dtmeDays: 522,
purchaseYield: 8.47,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 52263.30,
},
{
id: 'inv-bhr-6',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS1729875598',
securityType: 'GOP EuroBond 2027',
faceValue: 1.00,
amountInvested: 0.91,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '21-Aug-19',
maturityDate: '05-Dec-27',
originalPrice: 93.00,
dtmeDays: 522,
purchaseYield: 8.05,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 20616.89,
},
{
id: 'inv-bhr-7',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS3353874145',
securityType: 'GOP EuroBond 2029',
faceValue: 2.00,
amountInvested: 1.99,
couponRate: 6.98,
issueDate: '24-Apr-26',
purchaseDate: '30-Apr-26',
maturityDate: '24-Apr-29',
originalPrice: 99.54,
dtmeDays: 1028,
purchaseYield: 7.15,
mtmPrice: 99.75,
mtmYield: 7.07,
mtmPnL: 4077.60,
},
{
id: 'inv-bhr-8',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS3353874145',
securityType: 'GOP EuroBond 2029',
faceValue: 3.00,
amountInvested: 2.98,
couponRate: 6.98,
issueDate: '24-Apr-26',
purchaseDate: '01-May-26',
maturityDate: '24-Apr-29',
originalPrice: 99.40,
dtmeDays: 1028,
purchaseYield: 7.20,
mtmPrice: 99.75,
mtmYield: 7.07,
mtmPnL: 9970.66,
},
{
id: 'inv-bhr-9',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS3353874145',
securityType: 'GOP EuroBond 2029',
faceValue: 3.00,
amountInvested: 2.97,
couponRate: 6.98,
issueDate: '24-Apr-26',
purchaseDate: '04-May-26',
maturityDate: '24-Apr-29',
originalPrice: 99.14,
dtmeDays: 1028,
purchaseYield: 7.30,
mtmPrice: 99.75,
mtmYield: 7.07,
mtmPnL: 17425.15,
},
{
id: 'inv-bhr-10',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS3353874145',
securityType: 'GOP EuroBond 2029',
faceValue: 2.00,
amountInvested: 1.98,
couponRate: 6.98,
issueDate: '24-Apr-26',
purchaseDate: '18-May-26',
maturityDate: '24-Apr-29',
originalPrice: 99.15,
dtmeDays: 1028,
purchaseYield: 7.30,
mtmPrice: 99.75,
mtmYield: 7.07,
mtmPnL: 11915.00,
},
{
id: 'inv-bhr-11',
branchId: 'bahrain',
country: 'Bahrain',
category: 'AFS',
isin: 'XS3353874145',
securityType: 'GOP EuroBond 2029',
faceValue: 2.00,
amountInvested: 1.97,
couponRate: 6.98,
issueDate: '24-Apr-26',
purchaseDate: '19-May-26',
maturityDate: '24-Apr-29',
originalPrice: 98.58,
dtmeDays: 1028,
purchaseYield: 7.52,
mtmPrice: 99.75,
mtmYield: 7.07,
mtmPnL: 22512.17,
},
{
id: 'inv-bhr-12',
branchId: 'bahrain',
country: 'Bahrain',
category: 'HTM',
isin: 'BH0001827419',
securityType: 'Bahrain Govt Treasury Bills',
faceValue: 12.00,
amountInvested: 11.66,
couponRate: 5.25,
issueDate: '15-Feb-26',
purchaseDate: '15-Feb-26',
maturityDate: '15-Nov-26',
originalPrice: 97.16,
dtmeDays: 138,
purchaseYield: 5.40,
mtmPrice: 98.20,
mtmYield: 5.30,
mtmPnL: 124800,
},
// ==========================================
// BANGLADESH BRANCH PORTFOLIO
// ==========================================
{
id: 'inv-bgd-1',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD926021108',
securityType: '10 Yrs Govt T-Bond',
faceValue: 0.82,
amountInvested: 0.82,
couponRate: 7.59,
issueDate: '20-Jul-16',
purchaseDate: '21-Jul-16',
maturityDate: '20-Jul-26',
originalPrice: 101.308,
dtmeDays: 19,
purchaseYield: 6.89,
mtmPrice: 99.890,
mtmYield: 9.23,
mtmPnL: 1106,
},
{
id: 'inv-bgd-2',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD927251100',
securityType: '10 Yrs Govt T-Bond',
faceValue: 4.08,
amountInvested: 4.07,
couponRate: 6.77,
issueDate: '18-Jan-17',
purchaseDate: '18-Jan-27',
maturityDate: '18-Jan-27',
originalPrice: 104.842,
dtmeDays: 201,
purchaseYield: 6.80,
mtmPrice: 98.450,
mtmYield: 9.71,
mtmPnL: -62306,
},
{
id: 'inv-bgd-3',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD927251100',
securityType: '10 Yrs Govt T-Bond',
faceValue: 1.63,
amountInvested: 1.63,
couponRate: 6.77,
issueDate: '18-Jan-17',
purchaseDate: '19-Jul-17',
maturityDate: '18-Jan-27',
originalPrice: 99.785,
dtmeDays: 201,
purchaseYield: 6.82,
mtmPrice: 98.450,
mtmYield: 9.71,
mtmPnL: -24749,
},
{
id: 'inv-bgd-4',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD927011107',
securityType: '10 Yrs Govt T-Bond',
faceValue: 3.75,
amountInvested: 3.75,
couponRate: 7.00,
issueDate: '18-Oct-17',
purchaseDate: '20-Oct-17',
maturityDate: '18-Oct-27',
originalPrice: 99.642,
dtmeDays: 474,
purchaseYield: 7.00,
mtmPrice: 96.510,
mtmYield: 9.90,
mtmPnL: -130300,
},
{
id: 'inv-bgd-5',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HFT',
isin: 'BD927151151',
securityType: '15 Yrs Govt T-Bond',
faceValue: 0.04,
amountInvested: 0.04,
couponRate: 11.88,
issueDate: '19-Sep-12',
purchaseDate: '19-Sep-27',
maturityDate: '19-Sep-27',
originalPrice: 100.000,
dtmeDays: 445,
purchaseYield: 11.88,
mtmPrice: 102.240,
mtmYield: 9.86,
mtmPnL: 930,
},
{
id: 'inv-bgd-6',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD928271152',
securityType: '15 Yrs Govt T-Bond',
faceValue: 2.04,
amountInvested: 2.04,
couponRate: 12.29,
issueDate: '26-Dec-13',
purchaseDate: '26-Dec-13',
maturityDate: '26-Dec-28',
originalPrice: 99.531,
dtmeDays: 909,
purchaseYield: 12.35,
mtmPrice: 103.790,
mtmYield: 10.40,
mtmPnL: 86700,
},
{
id: 'inv-bgd-7',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD932121207',
securityType: '20 Yrs Govt T-Bond',
faceValue: 2.04,
amountInvested: 2.04,
couponRate: 12.16,
issueDate: '26-Sep-12',
purchaseDate: '25-Oct-12',
maturityDate: '25-Oct-32',
originalPrice: 100.000,
dtmeDays: 2279,
purchaseYield: 12.16,
mtmPrice: 107.880,
mtmYield: 10.32,
mtmPnL: 160752,
},
{
id: 'inv-bgd-8',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD934321201',
securityType: '20 Yrs Govt T-Bond',
faceValue: 2.04,
amountInvested: 2.04,
couponRate: 12.24,
issueDate: '26-Feb-14',
purchaseDate: '26-Feb-14',
maturityDate: '26-Feb-34',
originalPrice: 99.491,
dtmeDays: 2735,
purchaseYield: 12.30,
mtmPrice: 110.180,
mtmYield: 10.29,
mtmPnL: 211080,
},
{
id: 'inv-bgd-9',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD934481203',
securityType: '20 Yrs Govt T-Bond',
faceValue: 20.00,
amountInvested: 20.00,
couponRate: 12.12,
issueDate: '25-Jun-14',
purchaseDate: '25-Jun-14',
maturityDate: '25-Jun-34',
originalPrice: 100.000,
dtmeDays: 2916,
purchaseYield: 12.12,
mtmPrice: 109.900,
mtmYield: 10.27,
mtmPnL: 1980000,
},
{
id: 'inv-bgd-10',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD0926018326',
securityType: 'Bangladesh Treasury Bills 364D',
faceValue: 56.10,
amountInvested: 56.10,
couponRate: 9.20,
issueDate: '12-Jan-26',
purchaseDate: '12-Jan-26',
maturityDate: '12-Jan-27',
originalPrice: 99.65,
dtmeDays: 195,
purchaseYield: 9.21,
mtmPrice: 99.75,
mtmYield: 9.20,
mtmPnL: 56100,
},
{
id: 'inv-bgd-11',
branchId: 'bangladesh',
country: 'Bangladesh',
category: 'HTM',
isin: 'BD0936405267',
securityType: 'Bangladesh Govt Treasury Bills 182D',
faceValue: 20.00,
amountInvested: 20.00,
couponRate: 9.23,
issueDate: '21-Jul-26',
purchaseDate: '21-Jul-26',
maturityDate: '21-Jan-27',
originalPrice: 99.47,
dtmeDays: 172,
purchaseYield: 9.23,
mtmPrice: 99.47,
mtmYield: 9.23,
mtmPnL: 0,
},
// ==========================================
// EPZ BRANCH PORTFOLIO
// ==========================================
{
id: 'inv-epz-1',
branchId: 'epz',
country: 'EPZ',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'Pakistan Euro Bonds',
faceValue: 5.00,
amountInvested: 5.06,
couponRate: 7.38,
issueDate: '19-Apr-21',
purchaseDate: '19-Apr-21',
maturityDate: '19-Apr-31',
originalPrice: 101.21,
dtmeDays: 1753,
purchaseYield: 7.20,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -14709.74,
},
// ==========================================
// SOUTH KOREA (SEOUL) PORTFOLIO
// ==========================================
{
id: 'inv-kor-1',
branchId: 'south_korea',
country: 'South Korea',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP BOND',
faceValue: 2.00,
amountInvested: 1.77,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '25-Feb-25',
maturityDate: '08-Apr-31',
originalPrice: 88.25,
dtmeDays: 1742,
purchaseYield: 9.98,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: 242600.00,
},
{
id: 'inv-kor-2',
branchId: 'south_korea',
country: 'South Korea',
category: 'HTM',
isin: 'KR103101AG52',
securityType: 'Korea Monetary Stabilization Bond',
faceValue: 0.12,
amountInvested: 0.13,
couponRate: 2.72,
issueDate: '14-May-26',
purchaseDate: '14-May-26',
maturityDate: '16-Jul-27',
originalPrice: 99.89,
dtmeDays: 345,
purchaseYield: 2.72,
mtmPrice: 99.88,
mtmYield: 2.72,
mtmPnL: -120,
},
// ==========================================
// HONG KONG PORTFOLIO
// ==========================================
{
id: 'inv-hkg-1',
branchId: 'hk',
country: 'Hong Kong',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2031',
faceValue: 2.00,
amountInvested: 2.06,
couponRate: 7.38,
issueDate: '31-Mar-21',
purchaseDate: '12-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.89,
dtmeDays: 1742,
purchaseYield: 6.97,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -24682.75,
},
{
id: 'inv-hkg-2',
branchId: 'hk',
country: 'Hong Kong',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2031',
faceValue: 3.00,
amountInvested: 3.09,
couponRate: 7.38,
issueDate: '31-Mar-21',
purchaseDate: '12-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.85,
dtmeDays: 1742,
purchaseYield: 6.97,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -36332.78,
},
{
id: 'inv-hkg-3',
branchId: 'hk',
country: 'Hong Kong',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2031',
faceValue: 2.00,
amountInvested: 2.06,
couponRate: 7.38,
issueDate: '31-Mar-21',
purchaseDate: '12-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.77,
dtmeDays: 1742,
purchaseYield: 6.99,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -23289.68,
},
{
id: 'inv-hkg-4',
branchId: 'hk',
country: 'Hong Kong',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2031',
faceValue: 3.00,
amountInvested: 3.08,
couponRate: 7.38,
issueDate: '31-Mar-21',
purchaseDate: '09-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.67,
dtmeDays: 1742,
purchaseYield: 7.00,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -33163.04,
},
{
id: 'inv-hkg-5',
branchId: 'hk',
country: 'Hong Kong',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EuroBond 2031',
faceValue: 5.75,
amountInvested: 5.89,
couponRate: 7.38,
issueDate: '31-Mar-21',
purchaseDate: '22-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.43,
dtmeDays: 1742,
purchaseYield: 7.03,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -61925.00,
},
// ==========================================
// JAPAN (TOKYO) PORTFOLIO
// ==========================================
{
id: 'inv-jpn-1',
branchId: 'japan',
country: 'Japan',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EUROBOND 2031',
faceValue: 3.00,
amountInvested: 3.07,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '13-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.37,
dtmeDays: 1742,
purchaseYield: 7.04,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -28253.34,
},
{
id: 'inv-jpn-2',
branchId: 'japan',
country: 'Japan',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EUROBOND 2031',
faceValue: 4.50,
amountInvested: 4.60,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '13-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.30,
dtmeDays: 1742,
purchaseYield: 7.05,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -40494.56,
},
{
id: 'inv-jpn-3',
branchId: 'japan',
country: 'Japan',
category: 'AFS',
isin: 'XS2232319638',
securityType: 'GOP EUROBOND 2031',
faceValue: 5.00,
amountInvested: 5.12,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '13-Apr-21',
maturityDate: '08-Apr-31',
originalPrice: 102.30,
dtmeDays: 1742,
purchaseYield: 7.05,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: -44993.95,
},
// ==========================================
// SAUDI ARABIA (RIYADH) PORTFOLIO
// ==========================================
{
id: 'inv-ksa-1',
branchId: 'saudi_arabia',
country: 'Saudi Arabia',
category: 'HTM',
isin: 'XS2232319638',
securityType: 'PKSTAN 7.375 04/08/31 REGS',
faceValue: 2.00,
amountInvested: 1.80,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '14-Apr-26',
maturityDate: '08-Apr-31',
originalPrice: 89.95,
dtmeDays: 1742,
purchaseYield: 9.98,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: 208600,
},
{
id: 'inv-ksa-2',
branchId: 'saudi_arabia',
country: 'Saudi Arabia',
category: 'HTM',
isin: 'XS2232319638',
securityType: 'PKSTAN 7.375 04/08/31 REGS',
faceValue: 6.00,
amountInvested: 4.92,
couponRate: 7.38,
issueDate: '08-Apr-21',
purchaseDate: '14-Apr-26',
maturityDate: '08-Apr-31',
originalPrice: 82.08,
dtmeDays: 1742,
purchaseYield: 12.29,
mtmPrice: 100.38,
mtmYield: 7.28,
mtmPnL: 1098000,
},
{
id: 'inv-ksa-3',
branchId: 'saudi_arabia',
country: 'Saudi Arabia',
category: 'HTM',
isin: 'XS1729875598',
securityType: 'PKSTAN 6.875 05/12/27 REGS',
faceValue: 2.00,
amountInvested: 1.99,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '20-Apr-26',
maturityDate: '05-Dec-27',
originalPrice: 99.30,
dtmeDays: 522,
purchaseYield: 7.33,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 24000,
},
{
id: 'inv-ksa-4',
branchId: 'saudi_arabia',
country: 'Saudi Arabia',
category: 'HTM',
isin: 'XS1729875598',
securityType: 'PKSTAN 6.875 05/12/27 REGS',
faceValue: 24.06,
amountInvested: 23.78,
couponRate: 6.88,
issueDate: '05-Dec-17',
purchaseDate: '20-Apr-26',
maturityDate: '05-Dec-27',
originalPrice: 98.83,
dtmeDays: 522,
purchaseYield: 7.58,
mtmPrice: 100.50,
mtmYield: 6.50,
mtmPnL: 402000,
},
{
id: 'inv-ksa-5',
branchId: 'saudi_arabia',
country: 'Saudi Arabia',
category: 'HTM',
isin: 'SA152010YB=',
securityType: 'Saudi Arabia Government Bond',
faceValue: 14.00,
amountInvested: 14.00,
couponRate: 5.36,
issueDate: '26-Jul-26',
purchaseDate: '26-Jul-26',
maturityDate: '26-Jul-36',
originalPrice: 99.95,
dtmeDays: 3650,
purchaseYield: 5.36,
mtmPrice: 99.96,
mtmYield: 5.36,
mtmPnL: 1400,
},
// ==========================================
// GERMANY PORTFOLIO
// ==========================================
{
id: 'inv-deu-1',
branchId: 'germany',
country: 'Germany',
category: 'AFS',
isin: 'DE0001102580',
securityType: 'German Federal Treasury Bond (Bund)',
faceValue: 0.00,
amountInvested: 0.00,
couponRate: 2.10,
issueDate: '15-Mar-25',
purchaseDate: '15-Mar-25',
maturityDate: '15-Mar-30',
originalPrice: 100.00,
dtmeDays: 1320,
purchaseYield: 2.10,
mtmPrice: 100.15,
mtmYield: 2.08,
mtmPnL: 0,
},
// ==========================================
// AFGHANISTAN PORTFOLIO
// ==========================================
{
id: 'inv-afg-1',
branchId: 'afghanistan',
country: 'Afghanistan',
category: 'HTM',
isin: 'AFG990001284',
securityType: 'Da Afghanistan Bank Capital Note',
faceValue: 0.00,
amountInvested: 0.00,
couponRate: 4.50,
issueDate: '01-Jan-26',
purchaseDate: '01-Jan-26',
maturityDate: '01-Jul-26',
originalPrice: 100.00,
dtmeDays: 180,
purchaseYield: 4.50,
mtmPrice: 100.00,
mtmYield: 4.50,
mtmPnL: 0,
},
];

View file

@ -0,0 +1,694 @@
import { PlacementRecord, BorrowingRecord } from '../types';
export const DEFAULT_F48_RATE = 278.16;
export const DEFAULT_PLACEMENTS_SEED: PlacementRecord[] = [
// EPZ Branch Placements to Tokyo Branch (Inter Branch - matching Tokyo borrowings)
{
id: 'plc-epz-1',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 1.00,
equivalentUsdMn: 1.00,
equivalentPkrMn: 278.16,
ratePercent: 4.17,
placementDate: '08-Jul-26',
maturityDate: '07-Aug-26',
tenorDays: 30,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-2',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.14,
placementDate: '19-May-26',
maturityDate: '17-Aug-26',
tenorDays: 90,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-3',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.24,
placementDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-4',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 0.82,
equivalentUsdMn: 0.82,
equivalentPkrMn: 228.09,
ratePercent: 4.24,
placementDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-5',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 1.86,
equivalentUsdMn: 1.86,
equivalentPkrMn: 517.38,
ratePercent: 4.24,
placementDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-6',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.16,
placementDate: '15-Jul-26',
maturityDate: '17-Aug-26',
tenorDays: 33,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-epz-7',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 4.55,
equivalentUsdMn: 4.55,
equivalentPkrMn: 1265.63,
ratePercent: 4.17,
placementDate: '24-Jul-26',
maturityDate: '24-Aug-26',
tenorDays: 31,
limitBreached: false,
f48Rate: 278.16,
},
// EPZ Placement to Bangladesh
{
id: 'plc-epz-8',
period: '22-May-26',
branchId: 'epz',
counterpartyCountryBranch: 'Bangladesh / Dhaka',
counterpartyName: 'NBP BANGLADESH',
counterpartyBranchId: 'bangladesh',
type: 'Inter Branch',
approvedLimitMn: 180.00,
currency: 'USD',
amountActualMn: 158.30,
equivalentUsdMn: 158.30,
equivalentPkrMn: 44032.73,
ratePercent: 4.85,
placementDate: '01-May-26',
maturityDate: '30-Aug-26',
tenorDays: 121,
limitBreached: false,
f48Rate: 278.16,
},
// Bahrain Placements
{
id: 'plc-bhr-1',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Saudi Arabia / Riyadh',
counterpartyName: 'NBP KSA BRANCH',
counterpartyBranchId: 'saudi_arabia',
type: 'Inter Branch',
approvedLimitMn: 50.00,
currency: 'USD',
amountActualMn: 33.20,
equivalentUsdMn: 33.20,
equivalentPkrMn: 9234.91,
ratePercent: 5.10,
placementDate: '10-May-26',
maturityDate: '10-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-bhr-2',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP TOKYO BRANCH',
counterpartyBranchId: 'japan',
type: 'Inter Branch',
approvedLimitMn: 20.00,
currency: 'USD',
amountActualMn: 8.97,
equivalentUsdMn: 8.97,
equivalentPkrMn: 2495.10,
ratePercent: 4.30,
placementDate: '12-May-26',
maturityDate: '12-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-bhr-3',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Germany / Frankfurt',
counterpartyName: 'NBP GERMANY',
counterpartyBranchId: 'germany',
type: 'Inter Branch',
approvedLimitMn: 10.00,
currency: 'USD',
amountActualMn: 6.30,
equivalentUsdMn: 6.30,
equivalentPkrMn: 1752.41,
ratePercent: 4.50,
placementDate: '15-May-26',
maturityDate: '15-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-bhr-4',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Hong Kong / HK',
counterpartyName: 'NBP HONG KONG',
counterpartyBranchId: 'hk',
type: 'Inter Branch',
approvedLimitMn: 15.00,
currency: 'USD',
amountActualMn: 7.90,
equivalentUsdMn: 7.90,
equivalentPkrMn: 2197.46,
ratePercent: 4.60,
placementDate: '18-May-26',
maturityDate: '18-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-bhr-5',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Afghanistan / Kabul',
counterpartyName: 'NBP AFGHANISTAN',
counterpartyBranchId: 'afghanistan',
type: 'Inter Branch',
approvedLimitMn: 25.00,
currency: 'USD',
amountActualMn: 17.00,
equivalentUsdMn: 17.00,
equivalentPkrMn: 4728.72,
ratePercent: 5.25,
placementDate: '01-May-26',
maturityDate: '01-Nov-26',
tenorDays: 184,
limitBreached: false,
f48Rate: 278.16,
},
// Hong Kong Placements
{
id: 'plc-hk-1',
period: '22-May-26',
branchId: 'hk',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
approvedLimitMn: 80.00,
currency: 'USD',
amountActualMn: 61.10,
equivalentUsdMn: 61.10,
equivalentPkrMn: 16995.58,
ratePercent: 4.75,
placementDate: '05-May-26',
maturityDate: '05-Nov-26',
tenorDays: 184,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-hk-2',
period: '22-May-26',
branchId: 'hk',
counterpartyCountryBranch: 'Saudi Arabia / Riyadh',
counterpartyName: 'NBP KSA BRANCH',
counterpartyBranchId: 'saudi_arabia',
type: 'Inter Branch',
approvedLimitMn: 15.00,
currency: 'USD',
amountActualMn: 9.60,
equivalentUsdMn: 9.60,
equivalentPkrMn: 2670.34,
ratePercent: 4.90,
placementDate: '10-May-26',
maturityDate: '10-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
// Saudi Arabia Placements
{
id: 'plc-saudi-1',
period: '22-May-26',
branchId: 'saudi_arabia',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
approvedLimitMn: 50.00,
currency: 'USD',
amountActualMn: 45.30,
equivalentUsdMn: 45.30,
equivalentPkrMn: 12600.65,
ratePercent: 4.80,
placementDate: '12-May-26',
maturityDate: '12-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
{
id: 'plc-saudi-2',
period: '22-May-26',
branchId: 'saudi_arabia',
counterpartyCountryBranch: 'Saudi Hollandi Bank',
counterpartyName: 'Saudi Hollandi Bank',
type: 'Inter Bank',
approvedLimitMn: 15.00,
currency: 'USD',
amountActualMn: 9.60,
equivalentUsdMn: 9.60,
equivalentPkrMn: 2670.34,
ratePercent: 5.15,
placementDate: '20-May-26',
maturityDate: '20-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
// Germany Placement Outside Network
{
id: 'plc-deu-1',
period: '22-May-26',
branchId: 'germany',
counterpartyCountryBranch: 'Germany / Frankfurt',
counterpartyName: 'Deutsche Bank AG',
type: 'Inter Bank',
approvedLimitMn: 20.00,
currency: 'EUR',
amountActualMn: 14.60,
equivalentUsdMn: 15.90,
equivalentPkrMn: 4422.74,
ratePercent: 3.85,
placementDate: '15-May-26',
maturityDate: '15-Aug-26',
tenorDays: 92,
limitBreached: false,
f48Rate: 278.16,
},
];
export const DEFAULT_BORROWINGS_SEED: BorrowingRecord[] = [
// Tokyo Branch Borrowings from EPZ (Matches screenshot exactly!)
{
id: 'bor-jp-1',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 1.00,
equivalentUsdMn: 1.00,
equivalentPkrMn: 278.16,
ratePercent: 4.17,
borrowingDate: '08-Jul-26',
maturityDate: '07-Aug-26',
tenorDays: 30,
f48Rate: 278.16,
},
{
id: 'bor-jp-2',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.14,
borrowingDate: '19-May-26',
maturityDate: '17-Aug-26',
tenorDays: 90,
f48Rate: 278.16,
},
{
id: 'bor-jp-3',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.24,
borrowingDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
f48Rate: 278.16,
},
{
id: 'bor-jp-4',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 0.82,
equivalentUsdMn: 0.82,
equivalentPkrMn: 228.09,
ratePercent: 4.24,
borrowingDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
f48Rate: 278.16,
},
{
id: 'bor-jp-5',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 1.86,
equivalentUsdMn: 1.86,
equivalentPkrMn: 517.38,
ratePercent: 4.24,
borrowingDate: '29-Jul-26',
maturityDate: '28-Aug-26',
tenorDays: 30,
f48Rate: 278.16,
},
{
id: 'bor-jp-6',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 0.50,
equivalentUsdMn: 0.50,
equivalentPkrMn: 139.08,
ratePercent: 4.16,
borrowingDate: '15-Jul-26',
maturityDate: '17-Aug-26',
tenorDays: 33,
f48Rate: 278.16,
},
{
id: 'bor-jp-7',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Japan / Tokyo',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 4.55,
equivalentUsdMn: 4.55,
equivalentPkrMn: 1265.63,
ratePercent: 4.17,
borrowingDate: '24-Jul-26',
maturityDate: '24-Aug-26',
tenorDays: 31,
f48Rate: 278.16,
},
// Additional borrowing for Tokyo from Bahrain to total 18.70 Mn USD
{
id: 'bor-jp-8',
period: '22-May-26',
branchId: 'japan',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 8.97,
equivalentUsdMn: 8.97,
equivalentPkrMn: 2495.10,
ratePercent: 4.30,
borrowingDate: '12-May-26',
maturityDate: '12-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
// Bangladesh Borrowings
{
id: 'bor-bgd-1',
period: '22-May-26',
branchId: 'bangladesh',
counterpartyCountryBranch: 'Bangladesh / Dhaka',
counterpartyName: 'NBP EPZ KARACHI',
counterpartyBranchId: 'epz',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 158.30,
equivalentUsdMn: 158.30,
equivalentPkrMn: 44032.73,
ratePercent: 4.85,
borrowingDate: '01-May-26',
maturityDate: '30-Aug-26',
tenorDays: 121,
f48Rate: 278.16,
},
// Bahrain Borrowings
{
id: 'bor-bhr-1',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Hong Kong / HK',
counterpartyName: 'NBP HONG KONG',
counterpartyBranchId: 'hk',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 61.10,
equivalentUsdMn: 61.10,
equivalentPkrMn: 16995.58,
ratePercent: 4.75,
borrowingDate: '05-May-26',
maturityDate: '05-Nov-26',
tenorDays: 184,
f48Rate: 278.16,
},
{
id: 'bor-bhr-2',
period: '22-May-26',
branchId: 'bahrain',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'Arab Banking Corporation',
type: 'Inter Bank',
currency: 'USD',
amountActualMn: 140.40,
equivalentUsdMn: 140.40,
equivalentPkrMn: 39053.66,
ratePercent: 5.20,
borrowingDate: '10-May-26',
maturityDate: '10-Nov-26',
tenorDays: 184,
f48Rate: 278.16,
},
// KSA Borrowings
{
id: 'bor-saudi-1',
period: '22-May-26',
branchId: 'saudi_arabia',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 33.20,
equivalentUsdMn: 33.20,
equivalentPkrMn: 9234.91,
ratePercent: 5.10,
borrowingDate: '10-May-26',
maturityDate: '10-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
{
id: 'bor-saudi-2',
period: '22-May-26',
branchId: 'saudi_arabia',
counterpartyCountryBranch: 'Saudi Arabia / Riyadh',
counterpartyName: 'Al Rajhi Bank',
type: 'Inter Bank',
currency: 'USD',
amountActualMn: 45.00,
equivalentUsdMn: 45.00,
equivalentPkrMn: 12517.20,
ratePercent: 5.05,
borrowingDate: '15-May-26',
maturityDate: '15-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
// Germany Borrowings
{
id: 'bor-deu-1',
period: '22-May-26',
branchId: 'germany',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 6.30,
equivalentUsdMn: 6.30,
equivalentPkrMn: 1752.41,
ratePercent: 4.50,
borrowingDate: '15-May-26',
maturityDate: '15-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
// Hong Kong Borrowings
{
id: 'bor-hk-1',
period: '22-May-26',
branchId: 'hk',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 7.90,
equivalentUsdMn: 7.90,
equivalentPkrMn: 2197.46,
ratePercent: 4.60,
borrowingDate: '18-May-26',
maturityDate: '18-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
{
id: 'bor-hk-2',
period: '22-May-26',
branchId: 'hk',
counterpartyCountryBranch: 'Hong Kong / HK',
counterpartyName: 'HSBC Hong Kong',
type: 'Inter Bank',
currency: 'USD',
amountActualMn: 0.70,
equivalentUsdMn: 0.70,
equivalentPkrMn: 194.71,
ratePercent: 4.80,
borrowingDate: '20-May-26',
maturityDate: '20-Aug-26',
tenorDays: 92,
f48Rate: 278.16,
},
// Afghanistan Borrowings
{
id: 'bor-afg-1',
period: '22-May-26',
branchId: 'afghanistan',
counterpartyCountryBranch: 'Bahrain / Manama',
counterpartyName: 'NBP BAHRAIN',
counterpartyBranchId: 'bahrain',
type: 'Inter Branch',
currency: 'USD',
amountActualMn: 17.00,
equivalentUsdMn: 17.00,
equivalentPkrMn: 4728.72,
ratePercent: 5.25,
borrowingDate: '01-May-26',
maturityDate: '01-Nov-26',
tenorDays: 184,
f48Rate: 278.16,
}
];

890
src/data/seedData.ts Normal file
View file

@ -0,0 +1,890 @@
import { BalanceSheetItems, BranchId, BranchSubmission, User, SmtpConfig, VarianceComment } from '../types';
export const INITIAL_USERS: User[] = [
{
id: 'user-admin',
email: 'admin@networkbank.com',
name: 'Head Office Admin',
role: 'admin',
approved: true,
createdAt: new Date().toISOString(),
password: 'admin123',
mustChangePassword: false,
isPasswordChanged: true,
},
{
id: 'user-bahrain',
email: 'bahrain@networkbank.com',
name: 'Bahrain Branch Officer',
role: 'branch_user',
branchId: 'bahrain',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-bangladesh',
email: 'bangladesh@networkbank.com',
name: 'Bangladesh Branch Manager',
role: 'branch_user',
branchId: 'bangladesh',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-saudi',
email: 'saudi@networkbank.com',
name: 'Saudi Arabia Branch Lead',
role: 'branch_user',
branchId: 'saudi_arabia',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-germany',
email: 'germany@networkbank.com',
name: 'Germany Finance Head',
role: 'branch_user',
branchId: 'germany',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-hk',
email: 'hk@networkbank.com',
name: 'Hong Kong Finance Manager',
role: 'branch_user',
branchId: 'hk',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-japan',
email: 'japan@networkbank.com',
name: 'Japan Branch Controller',
role: 'branch_user',
branchId: 'japan',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-afghanistan',
email: 'afghanistan@networkbank.com',
name: 'Afghanistan Branch Officer',
role: 'branch_user',
branchId: 'afghanistan',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-epz',
email: 'epz@networkbank.com',
name: 'EPZ Finance Lead',
role: 'branch_user',
branchId: 'epz',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
{
id: 'user-southkorea',
email: 'southkorea@networkbank.com',
name: 'South Korea Liaison',
role: 'branch_user',
branchId: 'south_korea',
approved: true,
createdAt: new Date().toISOString(),
password: 'password123',
mustChangePassword: false,
isPasswordChanged: false,
},
// A pending branch user to demonstrate admin approval workflow
{
id: 'user-pending-1',
email: 'new.officer@networkbank.com',
name: 'John Doe (Pending Approval)',
role: 'branch_user',
branchId: 'germany',
approved: false,
createdAt: new Date(Date.now() - 3600000 * 24).toISOString(),
password: 'password123',
mustChangePassword: true,
isPasswordChanged: false,
}
];
export const INITIAL_SMTP_CONFIG: SmtpConfig = {
host: 'smtp.networkbank.com',
port: 587,
username: 'alerts@networkbank.com',
fromEmail: 'noreply-portal@networkbank.com',
useTls: true,
autoRemindersEnabled: true,
reminderFrequencyDays: 7,
};
// Raw baseline submissions for 15-May-26
export const RAW_15_MAY_26: Record<BranchId, BalanceSheetItems> = {
bahrain: {
balancesWithBanks: 0.9,
balanceCentralBank: 0.0,
placementHO: 76.2,
placementNetwork: 283.3,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 43.0,
investments: 48.0,
otherAssets: 5.4,
fiDepositBanksOnly: 13.5,
customerDeposit: 63.7,
depositsNbpNetwork: 0.0,
borrowingNetwork: 61.1,
borrowingOutsideNetwork: 140.4,
borrowingHO: 1.0,
headOfficeSupportFund: 79.7,
otherLiabilities: 35.9,
fullPledged: 0,
partialPledged: 0,
},
bangladesh: {
balancesWithBanks: 0.4,
balanceCentralBank: 7.3,
placementHO: 0.0,
placementNetwork: 28.9,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 50.8,
investments: 115.2,
otherAssets: 33.3,
fiDepositBanksOnly: 0.0,
customerDeposit: 4.3,
depositsNbpNetwork: 0.8,
borrowingNetwork: 158.3,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 15.1,
fullPledged: 115.2, // Investments pledged
partialPledged: 7.3, // Central bank partial pledged
},
saudi_arabia: {
balancesWithBanks: 0.2,
balanceCentralBank: 3.6,
placementHO: 27.0,
placementNetwork: 45.3,
placementOutsideNetwork: 9.6,
tradeAsset: 0.0,
netAdvances: 0.0,
investments: 47.9,
otherAssets: 2.0,
fiDepositBanksOnly: 0.2,
customerDeposit: 50.2,
depositsNbpNetwork: 0.0,
borrowingNetwork: 33.2,
borrowingOutsideNetwork: 45.0,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 2.5,
fullPledged: 0,
partialPledged: 47.9, // Partial Pledged Investments
},
germany: {
balancesWithBanks: 1.0,
balanceCentralBank: 12.0,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 15.9,
tradeAsset: 7.1,
netAdvances: 8.5,
investments: 0.0,
otherAssets: 0.2,
fiDepositBanksOnly: 1.4,
customerDeposit: 17.6,
depositsNbpNetwork: 12.0,
borrowingNetwork: 6.3,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 1.7,
otherLiabilities: 8.5,
fullPledged: 0,
partialPledged: 0,
},
hk: {
balancesWithBanks: 10.1,
balanceCentralBank: 0.0,
placementHO: 38.3,
placementNetwork: 70.7,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 6.3,
investments: 17.3,
otherAssets: 1.4,
fiDepositBanksOnly: 4.7,
customerDeposit: 109.4,
depositsNbpNetwork: 0.3,
borrowingNetwork: 7.9,
borrowingOutsideNetwork: 0.7,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.5,
fullPledged: 0,
partialPledged: 0,
},
japan: {
balancesWithBanks: 2.4,
balanceCentralBank: 19.2,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 0.0,
tradeAsset: 20.4,
netAdvances: 0.0,
investments: 12.8,
otherAssets: 1.2,
fiDepositBanksOnly: 0.4,
customerDeposit: 6.5,
depositsNbpNetwork: 3.8,
borrowingNetwork: 18.7,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 6.1,
otherLiabilities: 0.8,
fullPledged: 19.2, // Full pledged central bank
partialPledged: 0,
},
afghanistan: {
balancesWithBanks: 0.3,
balanceCentralBank: 21.4,
placementHO: 0.0,
placementNetwork: 10.5,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 0.0,
investments: 0.0,
otherAssets: 2.5,
fiDepositBanksOnly: 0.0,
customerDeposit: 16.4,
depositsNbpNetwork: 0.0,
borrowingNetwork: 0.0,
borrowingOutsideNetwork: 0.0,
borrowingHO: 1.4,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.5,
fullPledged: 0,
partialPledged: 0,
},
epz: {
balancesWithBanks: 0.0,
balanceCentralBank: 0.0,
placementHO: 58.6,
placementNetwork: 37.7,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 219.4,
investments: 4.9,
otherAssets: 12.9,
fiDepositBanksOnly: 0.0,
customerDeposit: 149.9,
depositsNbpNetwork: 0.0,
borrowingNetwork: 157.1,
borrowingOutsideNetwork: 0.0,
borrowingHO: 49.9,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.0,
fullPledged: 0,
partialPledged: 0,
},
south_korea: {
balancesWithBanks: 0.6,
balanceCentralBank: 2.9,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 4.5,
tradeAsset: 35.7,
netAdvances: 0.0,
investments: 1.9,
otherAssets: 1.3,
fiDepositBanksOnly: 0.0,
customerDeposit: 0.4,
depositsNbpNetwork: 0.0,
borrowingNetwork: 35.9,
borrowingOutsideNetwork: 2.9,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.6,
fullPledged: 0,
partialPledged: 0,
},
};
// Raw baseline submissions for 22-May-26 (Image 1)
export const RAW_22_MAY_26: Record<BranchId, BalanceSheetItems> = {
bahrain: {
balancesWithBanks: 0.8,
balanceCentralBank: 0.0,
placementHO: 54.3,
placementNetwork: 285.0,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 43.0,
investments: 61.8,
otherAssets: 6.5,
fiDepositBanksOnly: 13.5,
customerDeposit: 64.7,
depositsNbpNetwork: 0.0,
borrowingNetwork: 61.1,
borrowingOutsideNetwork: 133.5,
borrowingHO: 1.1,
headOfficeSupportFund: 79.7,
otherLiabilities: 35.9,
fullPledged: 0,
partialPledged: 0,
},
bangladesh: {
balancesWithBanks: 0.4,
balanceCentralBank: 7.3,
placementHO: 0.0,
placementNetwork: 28.9,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 50.8,
investments: 115.2,
otherAssets: 33.3,
fiDepositBanksOnly: 0.0,
customerDeposit: 4.3,
depositsNbpNetwork: 0.8,
borrowingNetwork: 158.28,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 15.1,
fullPledged: 115.2,
partialPledged: 7.3,
},
saudi_arabia: {
balancesWithBanks: 0.3,
balanceCentralBank: 3.7,
placementHO: 27.0,
placementNetwork: 45.3,
placementOutsideNetwork: 9.9,
tradeAsset: 0.0,
netAdvances: 0.0,
investments: 48.0,
otherAssets: 2.0,
fiDepositBanksOnly: 1.0,
customerDeposit: 49.7,
depositsNbpNetwork: 0.0,
borrowingNetwork: 33.2,
borrowingOutsideNetwork: 45.0,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 2.6,
fullPledged: 0,
partialPledged: 48.0,
},
germany: {
balancesWithBanks: 0.7,
balanceCentralBank: 8.2,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 23.1,
tradeAsset: 6.1,
netAdvances: 6.6,
investments: 0.0,
otherAssets: 0.2,
fiDepositBanksOnly: 1.4,
customerDeposit: 17.3,
depositsNbpNetwork: 12.6,
borrowingNetwork: 6.3,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 1.7,
otherLiabilities: 8.5,
fullPledged: 0,
partialPledged: 0,
},
hk: {
balancesWithBanks: 5.2,
balanceCentralBank: 0.0,
placementHO: 49.3,
placementNetwork: 70.7,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 11.6,
investments: 17.3,
otherAssets: 1.4,
fiDepositBanksOnly: 0.9,
customerDeposit: 124.4,
depositsNbpNetwork: 0.4,
borrowingNetwork: 7.9,
borrowingOutsideNetwork: 0.7,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.5,
fullPledged: 0,
partialPledged: 0,
},
japan: {
balancesWithBanks: 2.8,
balanceCentralBank: 17.2,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 0.0,
tradeAsset: 9.4,
netAdvances: 0.0,
investments: 12.8,
otherAssets: 1.2,
fiDepositBanksOnly: 0.5,
customerDeposit: 5.2,
depositsNbpNetwork: 3.1,
borrowingNetwork: 8.0,
borrowingOutsideNetwork: 0.0,
borrowingHO: 0.0,
headOfficeSupportFund: 6.1,
otherLiabilities: 0.8,
fullPledged: 17.2,
partialPledged: 12.8,
},
afghanistan: {
balancesWithBanks: 0.3,
balanceCentralBank: 21.3,
placementHO: 0.0,
placementNetwork: 10.6,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 0.0,
investments: 0.0,
otherAssets: 2.6,
fiDepositBanksOnly: 0.0,
customerDeposit: 16.5,
depositsNbpNetwork: 0.0,
borrowingNetwork: 0.0,
borrowingOutsideNetwork: 0.0,
borrowingHO: 1.4,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.5,
fullPledged: 0,
partialPledged: 0,
},
epz: {
balancesWithBanks: 0.0,
balanceCentralBank: 0.0,
placementHO: 20.8,
placementNetwork: 70.3,
placementOutsideNetwork: 0.0,
tradeAsset: 0.0,
netAdvances: 219.4,
investments: 4.9,
otherAssets: 12.1,
fiDepositBanksOnly: 0.0,
customerDeposit: 126.0,
depositsNbpNetwork: 0.0,
borrowingNetwork: 187.0,
borrowingOutsideNetwork: 0.0,
borrowingHO: 37.9,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.0,
fullPledged: 0,
partialPledged: 0,
},
south_korea: {
balancesWithBanks: 0.9,
balanceCentralBank: 2.9,
placementHO: 0.0,
placementNetwork: 0.0,
placementOutsideNetwork: 4.5,
tradeAsset: 48.7,
netAdvances: 0.0,
investments: 1.9,
otherAssets: 1.3,
fiDepositBanksOnly: 0.0,
customerDeposit: 0.3,
depositsNbpNetwork: 0.0,
borrowingNetwork: 48.4,
borrowingOutsideNetwork: 2.9,
borrowingHO: 0.0,
headOfficeSupportFund: 0.0,
otherLiabilities: 0.6,
fullPledged: 0,
partialPledged: 0,
},
};
// Initial Variance Comments for 22-May-26 vs 15-May-26 matching Image 3!
export const INITIAL_VARIANCE_COMMENTS_22_MAY: Record<BranchId, VarianceComment[]> = {
bahrain: [
{
lineItemKey: 'placementHO',
lineItemLabel: 'Placement - HO',
previousValue: 76.2,
newValue: 54.3,
variance: -21.9,
comment: 'NBP Bahrain: Due to external borrowing matured (previously borrowed for anticipated GoP syndication) + Eurobond purchases',
},
{
lineItemKey: 'investments',
lineItemLabel: 'Investments',
previousValue: 48.0,
newValue: 61.8,
variance: 13.8,
comment: 'NBP Bahrain: Recent purchases of Eurobond',
},
{
lineItemKey: 'borrowingOutsideNetwork',
lineItemLabel: 'Borrowing - Outside Network',
previousValue: 140.4,
newValue: 133.5,
variance: -6.9,
comment: 'NBP Bahrain: Borrowing from other banks due to anticipated GoP syndication matured',
}
],
bangladesh: [],
saudi_arabia: [],
germany: [
{
lineItemKey: 'balanceCentralBank',
lineItemLabel: 'Balance with Central Bank',
previousValue: 12.0,
newValue: 8.2,
variance: -3.8,
comment: 'NBP Germany: Transfer to commercial bank placements for yield optimization',
},
{
lineItemKey: 'placementOutsideNetwork',
lineItemLabel: 'Placement - Outside Network',
previousValue: 15.9,
newValue: 23.1,
variance: 7.2,
comment: 'NBP Germany: Placement of excess liquidity with local banks',
}
],
hk: [
{
lineItemKey: 'balancesWithBanks',
lineItemLabel: 'Balances with Banks (Onshore and Offshore)',
previousValue: 10.1,
newValue: 5.2,
variance: -4.9,
comment: 'NBP HK: Deployment into advances and customer loans',
},
{
lineItemKey: 'placementHO',
lineItemLabel: 'Placement - HO',
previousValue: 38.3,
newValue: 49.3,
variance: 11.0,
comment: 'NBP HK: Increase in Head Office placements',
},
{
lineItemKey: 'netAdvances',
lineItemLabel: 'Net Advances',
previousValue: 6.3,
newValue: 11.6,
variance: 5.3,
comment: 'NBP HK: Increase in advances (source: customer deposits - PNSC)',
},
{
lineItemKey: 'customerDeposit',
lineItemLabel: 'Customer Deposit',
previousValue: 109.4,
newValue: 124.4,
variance: 15.0,
comment: 'NBP HK: Increase in PNSC deposit',
}
],
japan: [
{
lineItemKey: 'tradeAsset',
lineItemLabel: 'Trade Asset',
previousValue: 20.4,
newValue: 9.4,
variance: -11.0,
comment: 'NBP Japan: Settlement of trade bills matured during the week',
},
{
lineItemKey: 'borrowingNetwork',
lineItemLabel: 'Borrowing - Network',
previousValue: 18.7,
newValue: 8.0,
variance: -10.7,
comment: 'NBP Japan: Partial repayment of inter-network borrowings',
}
],
afghanistan: [],
epz: [
{
lineItemKey: 'placementHO',
lineItemLabel: 'Placement - HO',
previousValue: 58.6,
newValue: 20.8,
variance: -37.8,
comment: 'NBP EPZ: Re-directed liquidity to network placement and customer drawdowns',
},
{
lineItemKey: 'placementNetwork',
lineItemLabel: 'Placement - Network',
previousValue: 37.7,
newValue: 70.3,
variance: 32.6,
comment: 'NBP EPZ: Short term inter-network funding placement',
},
{
lineItemKey: 'customerDeposit',
lineItemLabel: 'Customer Deposit',
previousValue: 149.9,
newValue: 126.0,
variance: -23.9,
comment: 'NBP EPZ: Commercial client trade payment withdrawal',
},
{
lineItemKey: 'borrowingNetwork',
lineItemLabel: 'Borrowing - Network',
previousValue: 157.1,
newValue: 187.0,
variance: 29.9,
comment: 'NBP EPZ: Network borrowing expansion for liquidity reserve',
},
{
lineItemKey: 'borrowingHO',
lineItemLabel: 'Borrowing - HO',
previousValue: 49.9,
newValue: 37.9,
variance: -12.0,
comment: 'NBP EPZ: Head office loan amortization',
}
],
south_korea: [
{
lineItemKey: 'tradeAsset',
lineItemLabel: 'Trade Asset',
previousValue: 35.7,
newValue: 48.7,
variance: 13.0,
comment: 'NBP South Korea: New letter of credit trade assets before winding down',
},
{
lineItemKey: 'borrowingNetwork',
lineItemLabel: 'Borrowing - Network',
previousValue: 35.9,
newValue: 48.4,
variance: 12.5,
comment: 'NBP South Korea: Short-term network liquidity line drawn',
}
]
};
import { CompositionBreakdown } from '../types';
export const DEFAULT_SEED_COMPOSITIONS: Record<BranchId, CompositionBreakdown> = {
bahrain: {
otherAssets: [
{ id: 'bhr-oa-1', description: 'Accrued Interest & Fee Receivables', amount: 1.2 },
{ id: 'bhr-oa-2', description: 'Prepaid Rent & Office Insurance', amount: 0.4 },
{ id: 'bhr-oa-3', description: 'Suspense & Clearing Items', amount: 0.3 },
],
otherLiabilities: [
{ id: 'bhr-ol-1', description: 'Accrued Expenses & Trade Payables', amount: 11.5 },
{ id: 'bhr-ol-2', description: 'Provision for Taxation & Statutory Duties', amount: 4.2 },
{ id: 'bhr-ol-3', description: 'Unearned Income & Deferred Fees', amount: 2.3 },
],
},
bangladesh: {
otherAssets: [
{ id: 'bgd-oa-1', description: 'Accrued Interest on Investments', amount: 18.5 },
{ id: 'bgd-oa-2', description: 'Tax Receivables & Advance Withholdings', amount: 9.8 },
{ id: 'bgd-oa-3', description: 'Office Premises & Equipment Assets', amount: 3.2 },
{ id: 'bgd-oa-4', description: 'Sundry Receivables', amount: 1.8 },
],
otherLiabilities: [
{ id: 'bgd-ol-1', description: 'Accrued Interest Payable on Borrowings', amount: 8.4 },
{ id: 'bgd-ol-2', description: 'Provision for Classified Advances & Bad Debts', amount: 4.5 },
{ id: 'bgd-ol-3', description: 'Taxes & Statutory Levies Payable', amount: 2.2 },
],
},
saudi_arabia: {
otherAssets: [
{ id: 'ksa-oa-1', description: 'Accrued Income on Interbank Placements', amount: 9.5 },
{ id: 'ksa-oa-2', description: 'Prepaid Operating & IT Expenses', amount: 4.1 },
{ id: 'ksa-oa-3', description: 'Security Deposits & Others', amount: 2.3 },
],
otherLiabilities: [
{ id: 'ksa-ol-1', description: 'Accrued Financial & Interest Expenses', amount: 18.2 },
{ id: 'ksa-ol-2', description: 'Zakat & Local Tax Provisions', amount: 7.8 },
{ id: 'ksa-ol-3', description: 'Sundry Creditors & Vendor Payables', amount: 4.6 },
],
},
germany: {
otherAssets: [
{ id: 'deu-oa-1', description: 'Accrued Trade Finance Fee Income', amount: 1.0 },
{ id: 'deu-oa-2', description: 'Prepaid IT Software Licences', amount: 0.6 },
],
otherLiabilities: [
{ id: 'deu-ol-1', description: 'Audit & Legal Services Provisions', amount: 2.5 },
{ id: 'deu-ol-2', description: 'Accrued Administrative & Utility Expenses', amount: 1.8 },
],
},
hk: {
otherAssets: [
{ id: 'hkg-oa-1', description: 'Accrued Interest on Export Trade Bills', amount: 3.4 },
{ id: 'hkg-oa-2', description: 'Deferred Tax Asset Realizations', amount: 1.8 },
],
otherLiabilities: [
{ id: 'hkg-ol-1', description: 'Accrued Interest on Customer Deposits', amount: 8.2 },
{ id: 'hkg-ol-2', description: 'Salaries & Staff Bonus Provisions', amount: 4.6 },
],
},
japan: {
otherAssets: [
{ id: 'jpn-oa-1', description: 'Accrued Bond Coupon Income', amount: 0.5 },
{ id: 'jpn-oa-2', description: 'Prepaid Tokyo Office Lease', amount: 0.3 },
],
otherLiabilities: [
{ id: 'jpn-ol-1', description: 'Withholding Tax Payable', amount: 0.8 },
{ id: 'jpn-ol-2', description: 'Utility & IT Services Payables', amount: 0.4 },
],
},
afghanistan: {
otherAssets: [
{ id: 'afg-oa-1', description: 'Cash in Transit & Inter-branch Suspense', amount: 0.8 },
{ id: 'afg-oa-2', description: 'Stationery & Security Escrow Deposits', amount: 0.4 },
],
otherLiabilities: [
{ id: 'afg-ol-1', description: 'Unearned Trade Commissions', amount: 0.5 },
{ id: 'afg-ol-2', description: 'Local Municipal Taxes Payable', amount: 0.4 },
],
},
epz: {
otherAssets: [
{ id: 'epz-oa-1', description: 'Export Processing Zone Duty Refund', amount: 1.6 },
{ id: 'epz-oa-2', description: 'Accrued Interest on HO Liquidity Funds', amount: 0.8 },
],
otherLiabilities: [],
},
south_korea: {
otherAssets: [
{ id: 'kor-oa-1', description: 'Winding Down Asset Liquidation Realization', amount: 0.9 },
{ id: 'kor-oa-2', description: 'Legal Escrow Deposit', amount: 0.4 },
],
otherLiabilities: [
{ id: 'kor-ol-1', description: 'Winding Down Staff Severance Provision', amount: 0.6 },
],
},
};
export function computeTotalAssets(items: BalanceSheetItems): number {
return (
items.balancesWithBanks +
items.balanceCentralBank +
items.placementHO +
items.placementNetwork +
items.placementOutsideNetwork +
items.tradeAsset +
items.netAdvances +
items.investments +
items.otherAssets
);
}
export function computeTotalLiabilities(items: BalanceSheetItems): number {
return (
items.fiDepositBanksOnly +
items.customerDeposit +
items.depositsNbpNetwork +
items.borrowingNetwork +
items.borrowingOutsideNetwork +
items.borrowingHO +
items.headOfficeSupportFund +
items.otherLiabilities
);
}
export function computeNetAssets(items: BalanceSheetItems): number {
return computeTotalAssets(items) - computeTotalLiabilities(items);
}
export function buildSubmissionsForPeriod(
period: string,
rawMap: Record<BranchId, BalanceSheetItems>,
commentsMap?: Record<BranchId, VarianceComment[]>
): BranchSubmission[] {
const branchIds = Object.keys(rawMap) as BranchId[];
return branchIds.map((bId) => {
const items = rawMap[bId];
const totalAssets = computeTotalAssets(items);
const totalLiab = computeTotalLiabilities(items);
const netAssets = totalAssets - totalLiab;
const totalLiabAndEquity = totalLiab + netAssets;
const diff = Math.abs(totalAssets - totalLiabAndEquity);
return {
id: `sub-${period}-${bId}`,
period,
branchId: bId,
submittedBy: `${bId}@networkbank.com`,
submittedAt: period === '22-May-26' ? '2026-05-22T08:30:00Z' : '2026-05-15T09:15:00Z',
status: 'submitted',
items,
compositions: DEFAULT_SEED_COMPOSITIONS[bId],
varianceComments: commentsMap ? (commentsMap[bId] || []) : [],
isBalanced: diff < 0.01,
differenceAmount: Number((totalAssets - totalLiabAndEquity).toFixed(2)),
};
});
}

172
src/index.css Normal file
View file

@ -0,0 +1,172 @@
@import "tailwindcss";
@layer utilities {
/* Holographic Glassmorphism */
.holo-card {
background: rgba(15, 23, 42, 0.94);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(6, 182, 212, 0.45);
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
inset 0 0 16px 0 rgba(6, 182, 212, 0.15);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.holo-card:hover {
border-color: rgba(6, 182, 212, 0.75);
box-shadow: 0 14px 45px 0 rgba(6, 182, 212, 0.3),
inset 0 0 24px 0 rgba(6, 182, 212, 0.25);
}
.holo-card-magenta {
background: rgba(15, 23, 42, 0.94);
backdrop-filter: blur(20px);
border: 1px solid rgba(236, 72, 153, 0.45);
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
inset 0 0 16px 0 rgba(236, 72, 153, 0.15);
}
.holo-card-magenta:hover {
border-color: rgba(236, 72, 153, 0.75);
box-shadow: 0 14px 45px 0 rgba(236, 72, 153, 0.3),
inset 0 0 24px 0 rgba(236, 72, 153, 0.25);
}
.holo-card-amber {
background: rgba(15, 23, 42, 0.94);
backdrop-filter: blur(20px);
border: 1px solid rgba(245, 158, 11, 0.45);
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
inset 0 0 16px 0 rgba(245, 158, 11, 0.15);
}
/* Neon Glow Text */
.glow-text-cyan {
text-shadow: 0 0 10px rgba(6, 182, 212, 0.6), 0 0 20px rgba(6, 182, 212, 0.4);
}
.glow-text-magenta {
text-shadow: 0 0 10px rgba(236, 72, 153, 0.6), 0 0 20px rgba(236, 72, 153, 0.4);
}
.glow-text-emerald {
text-shadow: 0 0 10px rgba(16, 185, 129, 0.6), 0 0 20px rgba(16, 185, 129, 0.4);
}
/* HUD Corner Reticles */
.hud-corner {
position: relative;
}
.hud-corner::before,
.hud-corner::after {
content: '';
position: absolute;
width: 6px;
height: 6px;
border-color: rgba(6, 182, 212, 0.6);
pointer-events: none;
}
.hud-corner::before {
top: 0;
left: 0;
border-top: 1.5px solid;
border-left: 1.5px solid;
}
.hud-corner::after {
bottom: 0;
right: 0;
border-bottom: 1.5px solid;
border-right: 1.5px solid;
}
/* Scanline overlay */
.scanlines {
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.25) 50%,
rgba(0, 0, 0, 0.25)
);
background-size: 100% 4px;
pointer-events: none;
}
}
/* Animations */
@keyframes holoScan {
0% { transform: translateY(-100%); }
100% { transform: translateY(1000%); }
}
@keyframes pulseGlow {
0%, 100% { opacity: 0.4; filter: drop-shadow(0 0 15px rgba(6, 182, 212, 0.5)); }
50% { opacity: 0.9; filter: drop-shadow(0 0 30px rgba(6, 182, 212, 0.9)); }
}
@keyframes floatUp {
0% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-8px) rotate(1deg); }
100% { transform: translateY(0px) rotate(0deg); }
}
.animate-holo-scan {
animation: holoScan 8s linear infinite;
}
.animate-pulse-glow {
animation: pulseGlow 3s ease-in-out infinite;
}
.animate-float-slow {
animation: floatUp 6s ease-in-out infinite;
}
/* Custom Cyber Scrollbar */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.8);
}
::-webkit-scrollbar-thumb {
background: rgba(6, 182, 212, 0.4);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(6, 182, 212, 0.8);
}
/* High Density Terminal Theme Classes */
.high-density-table th {
padding: 0.35rem 0.5rem !important;
font-size: 0.7rem !important;
font-weight: 700 !important;
letter-spacing: 0.05em !important;
text-transform: uppercase !important;
}
.high-density-table td {
padding: 0.3rem 0.5rem !important;
font-size: 0.725rem !important;
line-height: 1.2 !important;
}
.high-density-input {
padding: 0.25rem 0.5rem !important;
font-size: 0.75rem !important;
height: 1.85rem !important;
}
.high-density-card {
padding: 0.75rem !important;
}
.high-density-badge {
font-size: 0.625rem !important;
padding: 0.1rem 0.35rem !important;
letter-spacing: 0.025em !important;
}

10
src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -0,0 +1,341 @@
import {
BalanceSheetItems,
CompositionBreakdown,
TrialBalanceResult,
TrialBalanceLine,
VarianceComment,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
ExchangeRateRecord,
BRANCHES_LIST
} from '../types';
// Default FX Rates table relative to USD
export const DEFAULT_FX_RATES: Record<string, number> = {
USD: 1.0,
PKR: 278.16,
EUR: 0.92,
GBP: 0.78,
JPY: 155.40,
AED: 3.67,
SAR: 3.75,
BDT: 117.50,
CNY: 7.23,
HKD: 7.82,
CAD: 1.36,
};
/**
* Server-Side Accounting Engine
* Centralizes all Balance Sheet, Trial Balance, FX, and Double-Entry Calculations
*/
export class AccountingEngine {
/**
* Compute Total Assets from BalanceSheetItems
*/
static computeTotalAssets(items: BalanceSheetItems): number {
if (!items) return 0;
const sum =
(items.balancesWithBanks || 0) +
(items.balanceCentralBank || 0) +
(items.placementHO || 0) +
(items.placementNetwork || 0) +
(items.placementOutsideNetwork || 0) +
(items.tradeAsset || 0) +
(items.netAdvances || 0) +
(items.investments || 0) +
(items.otherAssets || 0);
return Number(sum.toFixed(2));
}
/**
* Compute Total Liabilities from BalanceSheetItems
*/
static computeTotalLiabilities(items: BalanceSheetItems): number {
if (!items) return 0;
const sum =
(items.fiDepositBanksOnly || 0) +
(items.customerDeposit || 0) +
(items.depositsNbpNetwork || 0) +
(items.borrowingNetwork || 0) +
(items.borrowingOutsideNetwork || 0) +
(items.borrowingHO || 0) +
(items.headOfficeSupportFund || 0) +
(items.otherLiabilities || 0);
return Number(sum.toFixed(2));
}
/**
* Compute Net Assets = Total Assets - Total Liabilities
*/
static computeNetAssets(items: BalanceSheetItems): number {
const assets = this.computeTotalAssets(items);
const liabilities = this.computeTotalLiabilities(items);
return Number((assets - liabilities).toFixed(2));
}
/**
* Generate a formal Trial Balance with Double-Entry Validation (Debit = Credit)
* Chart of Accounts (COA) mapping:
* - Asset lines -> Debit balances
* - Liability lines -> Credit balances
* - Head Office Fund / Net Assets -> Balancing Equity (Credit balance)
*/
static generateTrialBalance(
branchId: string,
branchName: string,
period: string,
items: BalanceSheetItems
): TrialBalanceResult {
const lines: TrialBalanceLine[] = [];
// Chart of Accounts (COA) Codes
const assetCoaMap: Record<keyof BalanceSheetItems, { code: string; name: string }> = {
balancesWithBanks: { code: '1010', name: 'Balances with Banks (Onshore/Offshore)' },
balanceCentralBank: { code: '1020', name: 'Balance with Central Bank' },
placementHO: { code: '1110', name: 'Placements - Head Office' },
placementNetwork: { code: '1120', name: 'Placements - Overseas Network' },
placementOutsideNetwork: { code: '1130', name: 'Placements - Inter-Bank Outside Network' },
tradeAsset: { code: '1210', name: 'Trade Finance Assets' },
netAdvances: { code: '1310', name: 'Net Advances & Loans' },
investments: { code: '1410', name: 'Investment Portfolio (HTM/AFS/HFT)' },
otherAssets: { code: '1510', name: 'Other Assets & Sundry Receivables' },
// dummy mappings for liability keys to avoid TS error
fiDepositBanksOnly: { code: '', name: '' },
customerDeposit: { code: '', name: '' },
depositsNbpNetwork: { code: '', name: '' },
borrowingNetwork: { code: '', name: '' },
borrowingOutsideNetwork: { code: '', name: '' },
borrowingHO: { code: '', name: '' },
headOfficeSupportFund: { code: '', name: '' },
otherLiabilities: { code: '', name: '' },
fullPledged: { code: '', name: '' },
partialPledged: { code: '', name: '' },
};
const liabilityCoaMap: Record<keyof BalanceSheetItems, { code: string; name: string }> = {
fiDepositBanksOnly: { code: '2010', name: 'FI Deposits (Banks Only)' },
customerDeposit: { code: '2020', name: 'Customer Deposits' },
depositsNbpNetwork: { code: '2110', name: 'Deposits from NBP Overseas Network' },
borrowingNetwork: { code: '2210', name: 'Borrowings - NBP Network' },
borrowingOutsideNetwork: { code: '2220', name: 'Borrowings - Inter-Bank Outside Network' },
borrowingHO: { code: '2230', name: 'Borrowings - Head Office' },
headOfficeSupportFund: { code: '2310', name: 'Head Office Capital Support Fund' },
otherLiabilities: { code: '2410', name: 'Other Liabilities & Accrued Provisions' },
// dummy mappings
balancesWithBanks: { code: '', name: '' },
balanceCentralBank: { code: '', name: '' },
placementHO: { code: '', name: '' },
placementNetwork: { code: '', name: '' },
placementOutsideNetwork: { code: '', name: '' },
tradeAsset: { code: '', name: '' },
netAdvances: { code: '', name: '' },
investments: { code: '', name: '' },
otherAssets: { code: '', name: '' },
fullPledged: { code: '', name: '' },
partialPledged: { code: '', name: '' },
};
let totalDebit = 0;
let totalCredit = 0;
// Process Assets (Debit Balance)
ASSET_ITEMS_CONFIG.forEach((cfg) => {
const val = items[cfg.key] || 0;
if (val > 0) {
const coa = assetCoaMap[cfg.key];
lines.push({
accountCode: coa.code,
accountName: coa.name,
type: 'Asset',
debitUsdMn: val,
creditUsdMn: 0,
netBalanceUsdMn: val,
});
totalDebit += val;
}
});
// Process Liabilities (Credit Balance)
LIABILITY_ITEMS_CONFIG.forEach((cfg) => {
const val = items[cfg.key] || 0;
if (val > 0) {
const coa = liabilityCoaMap[cfg.key];
lines.push({
accountCode: coa.code,
accountName: coa.name,
type: 'Liability',
debitUsdMn: 0,
creditUsdMn: val,
netBalanceUsdMn: -val,
});
totalCredit += val;
}
});
// Equity / Retained Earnings Balancing Line (Double Entry Principle)
const netAssets = totalDebit - totalCredit;
if (Math.abs(netAssets) > 0.0001) {
lines.push({
accountCode: '3010',
accountName: 'Branch Retained Capital & Reserves Equity',
type: 'Equity',
debitUsdMn: netAssets < 0 ? Math.abs(netAssets) : 0,
creditUsdMn: netAssets > 0 ? netAssets : 0,
netBalanceUsdMn: netAssets,
});
if (netAssets > 0) {
totalCredit += netAssets;
} else {
totalDebit += Math.abs(netAssets);
}
}
totalDebit = Number(totalDebit.toFixed(2));
totalCredit = Number(totalCredit.toFixed(2));
const diff = Number((totalDebit - totalCredit).toFixed(2));
return {
period,
branchId,
branchName,
lines,
totalDebitUsdMn: totalDebit,
totalCreditUsdMn: totalCredit,
differenceUsdMn: diff,
isDoubleEntryBalanced: Math.abs(diff) < 0.01,
computedAt: new Date().toISOString(),
};
}
/**
* Calculate Variances with Materiality Threshold
* Standard Banking Threshold: Default $1.00 Mn or $2.00 Mn
*/
static calculateVariancesWithMateriality(
prevItems: BalanceSheetItems,
newItems: BalanceSheetItems,
existingComments: VarianceComment[] = [],
materialityThresholdUsdMn: number = 1.0
): {
commentsRequired: { lineItemKey: keyof BalanceSheetItems; lineItemLabel: string; previousValue: number; newValue: number; variance: number }[];
autoGeneratedComments: VarianceComment[];
} {
const commentsRequired: { lineItemKey: keyof BalanceSheetItems; lineItemLabel: string; previousValue: number; newValue: number; variance: number }[] = [];
const autoGeneratedComments: VarianceComment[] = [];
const allConfigs = [...ASSET_ITEMS_CONFIG, ...LIABILITY_ITEMS_CONFIG];
allConfigs.forEach((cfg) => {
const pVal = prevItems ? prevItems[cfg.key] || 0 : 0;
const nVal = newItems ? newItems[cfg.key] || 0 : 0;
const variance = nVal - pVal;
if (Math.abs(variance) >= materialityThresholdUsdMn) {
commentsRequired.push({
lineItemKey: cfg.key,
lineItemLabel: cfg.label,
previousValue: pVal,
newValue: nVal,
variance: Number(variance.toFixed(2)),
});
const existing = existingComments.find((c) => c.lineItemKey === cfg.key);
autoGeneratedComments.push({
lineItemKey: cfg.key,
lineItemLabel: cfg.label,
previousValue: pVal,
newValue: nVal,
variance: Number(variance.toFixed(2)),
comment: existing?.comment || `Material variance of ${variance > 0 ? '+' : ''}${variance.toFixed(2)} USD Mn observed vs prior period.`,
});
}
});
return { commentsRequired, autoGeneratedComments };
}
/**
* FX Currency Conversion
* Convert amount from fromCurrency to toCurrency using current rates
*/
static convertCurrency(
amount: number,
fromCurrency: string = 'USD',
toCurrency: string = 'USD',
customRates?: Record<string, number>
): { amountConverted: number; rateUsed: number } {
const rates = { ...DEFAULT_FX_RATES, ...(customRates || {}) };
const fromRateToUsd = rates[fromCurrency.toUpperCase()] || 1.0;
const toRateToUsd = rates[toCurrency.toUpperCase()] || 1.0;
// First convert to USD
const usdAmount = amount / fromRateToUsd;
// Then convert to target currency
const targetAmount = usdAmount * toRateToUsd;
const effectiveCrossRate = toRateToUsd / fromRateToUsd;
return {
amountConverted: Number(targetAmount.toFixed(4)),
rateUsed: Number(effectiveCrossRate.toFixed(4)),
};
}
/**
* Server-Side Financial Input Validator
*/
static validateSubmissionInputs(
items: BalanceSheetItems,
compositions?: CompositionBreakdown
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!items) {
return { valid: false, errors: ['Missing balance sheet items object.'] };
}
const allConfigs = [...ASSET_ITEMS_CONFIG, ...LIABILITY_ITEMS_CONFIG];
allConfigs.forEach((cfg) => {
const val = items[cfg.key];
if (val === undefined || val === null || isNaN(val)) {
errors.push(`Field '${cfg.label}' (${cfg.key}) must be a valid number.`);
} else if (val < 0) {
errors.push(`Field '${cfg.label}' (${cfg.key}) cannot be negative (${val}).`);
} else if (val > 100000) {
errors.push(`Field '${cfg.label}' (${cfg.key}) exceeds maximum allowable threshold ($100,000 Mn).`);
}
});
// Validate Compositions sum matching line items
if (compositions) {
if (compositions.otherAssets && compositions.otherAssets.length > 0) {
const compSum = compositions.otherAssets.reduce((s, c) => s + (c.amount || 0), 0);
const lineVal = items.otherAssets || 0;
if (Math.abs(compSum - lineVal) > 0.05) {
errors.push(
`Other Assets breakdown total ($${compSum.toFixed(2)} Mn) does not match main line item ($${lineVal.toFixed(2)} Mn).`
);
}
}
if (compositions.otherLiabilities && compositions.otherLiabilities.length > 0) {
const compSum = compositions.otherLiabilities.reduce((s, c) => s + (c.amount || 0), 0);
const lineVal = items.otherLiabilities || 0;
if (Math.abs(compSum - lineVal) > 0.05) {
errors.push(
`Other Liabilities breakdown total ($${compSum.toFixed(2)} Mn) does not match main line item ($${lineVal.toFixed(2)} Mn).`
);
}
}
}
return {
valid: errors.length === 0,
errors,
};
}
}

345
src/types.ts Normal file
View file

@ -0,0 +1,345 @@
export type Role = 'admin' | 'branch_user';
export type BranchId =
| 'bahrain'
| 'bangladesh'
| 'saudi_arabia'
| 'germany'
| 'hk'
| 'japan'
| 'afghanistan'
| 'epz'
| 'south_korea';
export interface BranchInfo {
id: BranchId;
name: string;
code: string;
country: string;
isClosingDown?: boolean; // South Korea has special "Branches to be closed down" column header
contactEmail: string;
}
export interface User {
id: string;
email: string;
name: string;
role: Role;
branchId?: BranchId;
approved: boolean;
createdAt: string;
password?: string;
mustChangePassword?: boolean;
isPasswordChanged?: boolean;
}
export type PlacementBorrowingType = 'Inter Branch' | 'Inter Bank';
export interface PlacementRecord {
id: string;
period: string; // e.g. "22-May-26"
branchId: BranchId; // Placement branch e.g. "japan"
counterpartyCountryBranch: string; // e.g. "Japan / Tokyo" or counterparty location
counterpartyName: string; // e.g. "NBP EPZ KARACHI" or "Bank of Tokyo Mitsubishi"
counterpartyBranchId?: BranchId; // mapped counterparty branch ID if Inter Branch e.g. "epz"
type: PlacementBorrowingType; // 'Inter Branch' or 'Inter Bank'
approvedLimitMn?: number; // e.g. 5.00
currency: string; // e.g. "USD", "JPY", "EUR", "PKR"
amountActualMn: number; // e.g. 1.00
equivalentUsdMn: number; // e.g. 1.00
equivalentPkrMn: number; // e.g. 278.16
ratePercent: number; // e.g. 4.17
placementDate: string; // e.g. "08-Jul-26"
maturityDate: string; // e.g. "07-Aug-26"
tenorDays: number; // e.g. 30
limitBreached?: boolean;
f48Rate?: number; // FX rate e.g. 278.16
updatedAt?: string;
updatedBy?: string;
}
export interface BorrowingRecord {
id: string;
period: string; // e.g. "22-May-26"
branchId: BranchId; // Borrowing branch e.g. "japan"
counterpartyCountryBranch: string; // e.g. "Japan / Tokyo"
counterpartyName: string; // e.g. "NBP EPZ KARACHI"
counterpartyBranchId?: BranchId; // mapped counterparty branch ID if Inter Branch e.g. "epz"
type: PlacementBorrowingType; // 'Inter Branch' or 'Inter Bank'
currency: string; // e.g. "USD"
amountActualMn: number; // e.g. 1.00
equivalentUsdMn: number; // e.g. 1.00
equivalentPkrMn: number; // e.g. 278.16
ratePercent: number; // e.g. 4.17
borrowingDate: string; // e.g. "08-Jul-26"
maturityDate: string; // e.g. "07-Aug-26"
tenorDays: number; // e.g. 30
f48Rate?: number; // FX rate e.g. 278.16
updatedAt?: string;
updatedBy?: string;
}
export interface PlacementsBorrowingsReconciliation {
branchId: BranchId;
branchName: string;
placementsNetworkTotal: number; // Inter-Branch sum
placementsOutsideTotal: number; // Inter-Bank sum
placementsTotal: number;
bsPlacementNetwork: number;
bsPlacementOutside: number;
placementDifference: number;
isPlacementMatched: boolean;
borrowingsNetworkTotal: number; // Inter-Branch sum
borrowingsOutsideTotal: number; // Inter-Bank sum
borrowingsTotal: number;
bsBorrowingNetwork: number;
bsBorrowingOutside: number;
borrowingDifference: number;
isBorrowingMatched: boolean;
}
export interface InterBranchPairwiseMismatch {
id: string;
placingBranchId: BranchId;
placingBranchName: string;
borrowingBranchId: BranchId;
borrowingBranchName: string;
placementUsdMn: number;
borrowingUsdMn: number;
differenceUsdMn: number;
status: 'matched' | 'mismatch' | 'missing_counterparty';
details: string;
}
export type InvestmentCategory = 'AFS' | 'HTM' | 'HFT' | 'TBills' | 'Bonds' | 'Eurobond' | 'Sukuk' | 'Shares' | 'Other';
export interface InvestmentSecurity {
id: string;
branchId: BranchId;
country: string;
category: InvestmentCategory;
currency?: string; // Deal currency e.g. USD, PKR, EUR, GBP, JPY
amountActualMn?: number; // Face value / cost in actual currency
isin: string;
securityType: string; // e.g. "10 Yrs Govt T-Bond", "GOP EuroBond 2029", "T-Bills", "PAKISTAN SUKUK-7.95 PCT"
faceValue: number; // In USD Mio
amountInvested: number; // In USD Mio (Cost / Book Value)
couponRate: number; // % e.g. 7.95
issueDate: string; // e.g. "31-Jan-22"
purchaseDate: string; // e.g. "31-Jan-22"
maturityDate: string; // e.g. "31-Jan-29"
originalPrice: number; // e.g. 100.00 (% of par)
dtmeDays?: number; // Days to maturity
purchaseYield?: number; // %
mtmPrice?: number; // MTM price (% of par, set by HO)
mtmYield?: number; // MTM yield % (set/calculated by HO)
mtmPnL?: number; // Calculated PnL in USD
updatedAt?: string;
updatedBy?: string;
}
export interface InvestmentReconciliation {
branchId: BranchId;
branchName: string;
portfolioTotalInvested: number; // USD Mio
balanceSheetInvestments: number; // USD Mio
difference: number; // USD Mio
isMatched: boolean;
securitiesCount: number;
}
export interface BalanceSheetItems {
// Assets
balancesWithBanks: number;
balanceCentralBank: number;
placementHO: number;
placementNetwork: number;
placementOutsideNetwork: number;
tradeAsset: number;
netAdvances: number;
investments: number;
otherAssets: number;
// Liabilities
fiDepositBanksOnly: number;
customerDeposit: number;
depositsNbpNetwork: number;
borrowingNetwork: number;
borrowingOutsideNetwork: number;
borrowingHO: number;
headOfficeSupportFund: number;
otherLiabilities: number;
// Mandatory Pledged Requirements (In Mn)
fullPledged: number;
partialPledged: number;
}
export interface VarianceComment {
lineItemKey: keyof BalanceSheetItems;
lineItemLabel: string;
previousValue: number;
newValue: number;
variance: number;
comment: string;
}
export interface CompositionItem {
id: string;
description: string;
amount: number; // in USD Mn
}
export interface CompositionBreakdown {
otherAssets?: CompositionItem[];
otherLiabilities?: CompositionItem[];
}
export interface ExchangeRateRecord {
id: string;
period: string;
currency: string; // e.g. USD, PKR, EUR, GBP, JPY, AED, SAR, BDT, CNY, HKD, CAD
rateToUsd: number; // 1 USD = X Currency units
rateToPkr: number; // 1 Currency = Y PKR
effectiveDate: string;
updatedBy?: string;
updatedAt?: string;
}
export interface AuditLogEntry {
id: string;
timestamp: string;
userId: string;
userName: string;
userRole: string;
branchId?: BranchId | string;
action: string;
entityType: 'submission' | 'placement' | 'borrowing' | 'investment' | 'fx_rate' | 'period_lock' | 'liquidity_gap';
entityId?: string;
details: string;
previousValue?: any;
newValue?: any;
}
export interface TrialBalanceLine {
accountCode: string;
accountName: string;
type: 'Asset' | 'Liability' | 'Equity';
debitUsdMn: number;
creditUsdMn: number;
netBalanceUsdMn: number;
}
export interface TrialBalanceResult {
period: string;
branchId: string;
branchName: string;
lines: TrialBalanceLine[];
totalDebitUsdMn: number;
totalCreditUsdMn: number;
differenceUsdMn: number;
isDoubleEntryBalanced: boolean;
computedAt: string;
}
export type MakerCheckerStatus = 'draft' | 'submitted' | 'reviewed' | 'approved' | 'locked';
export interface PeriodLockRecord {
period: string;
branchId: BranchId | 'all';
isLocked: boolean;
lockedBy?: string;
lockedAt?: string;
reason?: string;
}
export interface BranchSubmission {
id: string;
period: string; // e.g. "22-May-26"
branchId: BranchId;
submittedBy: string;
submittedAt: string;
status: MakerCheckerStatus;
version?: number; // Optimistic locking
reviewedBy?: string;
reviewedAt?: string;
approvedBy?: string;
approvedAt?: string;
lockedBy?: string;
lockedAt?: string;
items: BalanceSheetItems;
compositions?: CompositionBreakdown;
varianceComments: VarianceComment[];
isBalanced: boolean;
differenceAmount: number;
}
export interface AutoCheckResult {
id: string;
title: string;
category: 'Inter-Network Placements vs Borrowings' | 'HO Placement vs HO Borrowings' | 'Network Deposits' | 'Balance Sheet Reconciliation';
status: 'pass' | 'fail' | 'warning';
description: string;
expectedValue: number;
actualValue: number;
difference: number;
flaggedBranches: BranchId[];
details: string;
}
export interface SmtpConfig {
host: string;
port: number;
username: string;
fromEmail: string;
useTls: boolean;
autoRemindersEnabled: boolean;
reminderFrequencyDays: number;
}
export interface EmailLog {
id: string;
recipientEmail: string;
branchId?: BranchId;
subject: string;
body: string;
sentAt: string;
status: 'sent' | 'simulated' | 'failed';
triggerType: 'manual_reminder' | 'auto_reminder' | 'submission_alert' | 'variance_flag';
}
export const BRANCHES_LIST: BranchInfo[] = [
{ id: 'bahrain', name: 'Bahrain', code: 'BHR', country: 'Bahrain', contactEmail: 'bahrain@networkbank.com' },
{ id: 'bangladesh', name: 'Bangladesh', code: 'BGD', country: 'Bangladesh', contactEmail: 'bangladesh@networkbank.com' },
{ id: 'saudi_arabia', name: 'Saudi Arabia', code: 'KSA', country: 'Saudi Arabia', contactEmail: 'saudi@networkbank.com' },
{ id: 'germany', name: 'Germany', code: 'DEU', country: 'Germany', contactEmail: 'germany@networkbank.com' },
{ id: 'hk', name: 'Hong Kong', code: 'HKG', country: 'Hong Kong', contactEmail: 'hk@networkbank.com' },
{ id: 'japan', name: 'Japan', code: 'JPN', country: 'Japan', contactEmail: 'japan@networkbank.com' },
{ id: 'afghanistan', name: 'Afghanistan', code: 'AFG', country: 'Afghanistan', contactEmail: 'afghanistan@networkbank.com' },
{ id: 'epz', name: 'EPZ', code: 'EPZ', country: 'EPZ', contactEmail: 'epz@networkbank.com' },
{ id: 'south_korea', name: 'South Korea', code: 'KOR', country: 'South Korea', isClosingDown: true, contactEmail: 'southkorea@networkbank.com' },
];
export const ASSET_ITEMS_CONFIG: { key: keyof BalanceSheetItems; label: string }[] = [
{ key: 'balancesWithBanks', label: 'Balances with Banks (Onshore and Offshore)' },
{ key: 'balanceCentralBank', label: 'Balance with Central Bank' },
{ key: 'placementHO', label: 'Placement - HO' },
{ key: 'placementNetwork', label: 'Placement - Network' },
{ key: 'placementOutsideNetwork', label: 'Placement - Outside Network' },
{ key: 'tradeAsset', label: 'Trade Asset' },
{ key: 'netAdvances', label: 'Net Advances' },
{ key: 'investments', label: 'Investments' },
{ key: 'otherAssets', label: 'Other Assets' },
];
export const LIABILITY_ITEMS_CONFIG: { key: keyof BalanceSheetItems; label: string }[] = [
{ key: 'fiDepositBanksOnly', label: 'FI Deposit - Banks only' },
{ key: 'customerDeposit', label: 'Customer Deposit' },
{ key: 'depositsNbpNetwork', label: 'Deposits from NBP Network' },
{ key: 'borrowingNetwork', label: 'Borrowing - Network' },
{ key: 'borrowingOutsideNetwork', label: 'Borrowing - Outside Network' },
{ key: 'borrowingHO', label: 'Borrowing - HO' },
{ key: 'headOfficeSupportFund', label: 'Head Office Support Fund' },
{ key: 'otherLiabilities', label: 'Other Liabilities' },
];

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

22
vite.config.ts Normal file
View file

@ -0,0 +1,22 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig} from 'vite';
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
watch: process.env.DISABLE_HMR === 'true' ? null : {},
},
};
});