Initial Commit
This commit is contained in:
commit
541f2ac0c0
31 changed files with 13917 additions and 0 deletions
9
.env.example
Normal file
9
.env.example
Normal 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
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
20
README.md
Normal file
20
README.md
Normal 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
765
bun.lock
Normal 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
13
index.html
Normal 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
6
metadata.json
Normal 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
41
package.json
Normal 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
0
portal-data.json
Normal file
225
src/App.tsx
Normal file
225
src/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
591
src/components/AdminUserManagement.tsx
Normal file
591
src/components/AdminUserManagement.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
547
src/components/AuthModal.tsx
Normal file
547
src/components/AuthModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
232
src/components/AutoChecksView.tsx
Normal file
232
src/components/AutoChecksView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
928
src/components/BranchPortalView.tsx
Normal file
928
src/components/BranchPortalView.tsx
Normal 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 (> 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">
|
||||||
|
> $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 (> 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">
|
||||||
|
> $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 (> 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 ≤ {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>
|
||||||
|
);
|
||||||
|
};
|
||||||
749
src/components/ConsolidatedView.tsx
Normal file
749
src/components/ConsolidatedView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
245
src/components/DashboardAnalytics.tsx
Normal file
245
src/components/DashboardAnalytics.tsx
Normal 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 (>$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>
|
||||||
|
);
|
||||||
|
};
|
||||||
339
src/components/HolographicCanvas.tsx
Normal file
339
src/components/HolographicCanvas.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
430
src/components/InteractiveWorldMap.tsx
Normal file
430
src/components/InteractiveWorldMap.tsx
Normal 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 (>$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>
|
||||||
|
);
|
||||||
|
};
|
||||||
1027
src/components/InvestmentsPortfolioView.tsx
Normal file
1027
src/components/InvestmentsPortfolioView.tsx
Normal file
File diff suppressed because it is too large
Load diff
948
src/components/LiquidityGapView.tsx
Normal file
948
src/components/LiquidityGapView.tsx
Normal 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
626
src/components/Navbar.tsx
Normal 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;
|
||||||
1240
src/components/PlacementsBorrowingsView.tsx
Normal file
1240
src/components/PlacementsBorrowingsView.tsx
Normal file
File diff suppressed because it is too large
Load diff
859
src/data/investmentSeedData.ts
Normal file
859
src/data/investmentSeedData.ts
Normal 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,
|
||||||
|
},
|
||||||
|
];
|
||||||
694
src/data/placementsBorrowingsSeedData.ts
Normal file
694
src/data/placementsBorrowingsSeedData.ts
Normal 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
890
src/data/seedData.ts
Normal 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
172
src/index.css
Normal 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
10
src/main.tsx
Normal 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>,
|
||||||
|
);
|
||||||
341
src/services/accountingEngine.ts
Normal file
341
src/services/accountingEngine.ts
Normal 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
345
src/types.ts
Normal 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
26
tsconfig.json
Normal 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
22
vite.config.ts
Normal 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 : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue