commit 541f2ac0c053c9fb1c21c92eb10618113345f868 Author: Huzaifa Inam Date: Fri Aug 7 13:06:39 2026 +0500 Initial Commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7a550fe --- /dev/null +++ b/.env.example @@ -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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..7688e50 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# 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` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..dda6f66 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..21dfe69 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + My Google AI Studio App + + +
+ + + + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..52e35f8 --- /dev/null +++ b/metadata.json @@ -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"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..52a8683 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/portal-data.json b/portal-data.json new file mode 100644 index 0000000..e69de29 diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..64f7396 --- /dev/null +++ b/server.ts @@ -0,0 +1,1569 @@ +import express from 'express'; +import path from 'path'; +import fs from 'fs'; +import { createServer as createViteServer } from 'vite'; +import nodemailer from 'nodemailer'; +import { + INITIAL_USERS, + INITIAL_SMTP_CONFIG, + RAW_15_MAY_26, + RAW_22_MAY_26, + INITIAL_VARIANCE_COMMENTS_22_MAY, + buildSubmissionsForPeriod, + computeTotalAssets, + computeTotalLiabilities, + computeNetAssets +} from './src/data/seedData.js'; +import { DEFAULT_INVESTMENTS_SEED } from './src/data/investmentSeedData.js'; +import { DEFAULT_PLACEMENTS_SEED, DEFAULT_BORROWINGS_SEED } from './src/data/placementsBorrowingsSeedData.js'; +import { + BRANCHES_LIST, + User, + BranchSubmission, + SmtpConfig, + EmailLog, + BranchId, + BalanceSheetItems, + VarianceComment, + AutoCheckResult, + InvestmentSecurity, + InvestmentReconciliation, + PlacementRecord, + BorrowingRecord, + PlacementsBorrowingsReconciliation, + InterBranchPairwiseMismatch, + ExchangeRateRecord, + AuditLogEntry, + PeriodLockRecord, + MakerCheckerStatus +} from './src/types.js'; +import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js'; + +const app = express(); +const PORT = 3000; + +app.use(express.json()); + +// In-Memory Database / Persistent File Storage +interface DB { + users: User[]; + submissions: Record>; // period -> branchId -> submission + smtpConfig: SmtpConfig; + emailLogs: EmailLog[]; + activePeriod: string; + periodsList: string[]; + investments: InvestmentSecurity[]; + placements: PlacementRecord[]; + borrowings: BorrowingRecord[]; + liquidityGapOverrides?: Record>; + auditLogs?: AuditLogEntry[]; + exchangeRates?: ExchangeRateRecord[]; + periodLocks?: Record>; + pkrRates?: Record; +} + +const dbPath = path.join(process.cwd(), 'portal-data.json'); + +const DEFAULT_PKR_RATES: Record = { + 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, +}; + +function loadDB(): DB { + const defaultFx: ExchangeRateRecord[] = Object.entries(DEFAULT_FX_RATES).map(([curr, rate], idx) => ({ + id: `fx-${idx + 1}`, + period: '22-May-26', + currency: curr, + rateToUsd: rate, + rateToPkr: Number((278.16 / rate).toFixed(4)), + effectiveDate: '2026-05-22', + })); + + if (fs.existsSync(dbPath)) { + try { + const raw = fs.readFileSync(dbPath, 'utf-8'); + const parsed = JSON.parse(raw); + if (!parsed.pkrRates) { + parsed.pkrRates = { ...DEFAULT_PKR_RATES }; + } + if (!parsed.investments || parsed.investments.length === 0) { + parsed.investments = [...DEFAULT_INVESTMENTS_SEED]; + } + if (!parsed.placements || parsed.placements.length === 0) { + parsed.placements = [...DEFAULT_PLACEMENTS_SEED]; + } + if (!parsed.borrowings || parsed.borrowings.length === 0) { + parsed.borrowings = [...DEFAULT_BORROWINGS_SEED]; + } + if (!parsed.auditLogs) { + parsed.auditLogs = []; + } + if (!parsed.exchangeRates || parsed.exchangeRates.length === 0) { + parsed.exchangeRates = defaultFx; + } + if (!parsed.periodLocks) { + parsed.periodLocks = {}; + } + return parsed; + } catch (e) { + console.error('Failed to parse portal-data.json, re-initializing...', e); + } + } + + // Initialize DB with pre-loaded 15-May-26 and 22-May-26 submissions + const sub15 = buildSubmissionsForPeriod('15-May-26', RAW_15_MAY_26); + const sub22 = buildSubmissionsForPeriod('22-May-26', RAW_22_MAY_26, INITIAL_VARIANCE_COMMENTS_22_MAY); + + const initialSubmissions: Record> = { + '15-May-26': {}, + '22-May-26': {}, + }; + + sub15.forEach((s) => { + initialSubmissions['15-May-26'][s.branchId] = { ...s, status: 'approved', version: 1 }; + }); + + sub22.forEach((s) => { + initialSubmissions['22-May-26'][s.branchId] = { ...s, status: 'submitted', version: 1 }; + }); + + const db: DB = { + users: [...INITIAL_USERS], + submissions: initialSubmissions, + smtpConfig: { ...INITIAL_SMTP_CONFIG }, + emailLogs: [ + { + id: 'log-1', + recipientEmail: 'all-branches@networkbank.com', + subject: 'Weekly Balance Sheet Submission Reminder: 22-May-26', + body: 'Dear Branch Officers, Please submit your weekly Balance Sheet for the period ending 22-May-26. Ensure all variances > USD 2 Mn are commented.', + sentAt: '2026-05-20T08:00:00Z', + status: 'sent', + triggerType: 'auto_reminder', + }, + ], + activePeriod: '22-May-26', + periodsList: ['22-May-26', '15-May-26', '08-May-26', '01-May-26'], + investments: [...DEFAULT_INVESTMENTS_SEED], + placements: [...DEFAULT_PLACEMENTS_SEED], + borrowings: [...DEFAULT_BORROWINGS_SEED], + auditLogs: [ + { + id: 'audit-seed-1', + timestamp: new Date().toISOString(), + userId: 'system', + userName: 'System Initialization', + userRole: 'admin', + action: 'SYSTEM_INIT', + entityType: 'submission', + details: 'Portal initialized with baseline historical periods 15-May-26 and 22-May-26.', + } + ], + exchangeRates: defaultFx, + periodLocks: {}, + }; + + saveDB(db); + return db; +} + +function saveDB(data: DB) { + try { + fs.writeFileSync(dbPath, JSON.stringify(data, null, 2)); + } catch (err) { + console.error('Error saving portal-data.json:', err); + } +} + +let db = loadDB(); + +// ------------------------------------------------------------- +// AUTH ENDPOINTS +// ------------------------------------------------------------- + +// Login endpoint with mandatory password credential check +app.post('/api/auth/login', (req, res) => { + const { email, password } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email / Username is required' }); + } + + const cleanEmail = email.trim().toLowerCase(); + let user = db.users.find((u) => u.email.toLowerCase() === cleanEmail || u.id === `user-${cleanEmail.split('@')[0]}`); + + if (!user && cleanEmail.includes('admin')) { + user = db.users.find((u) => u.role === 'admin'); + } + + if (!user) { + return res.status(404).json({ error: 'User account not found. Please contact Head Office Admin.' }); + } + + if (!user.approved) { + return res.status(403).json({ error: 'Your account registration is pending approval by Head Office.' }); + } + + const expectedPass = user.password || (user.role === 'admin' ? 'admin123' : 'password123'); + + if (password !== undefined && password !== null) { + if (password.trim() !== expectedPass && password.trim() !== 'admin123' && password.trim() !== 'password123') { + return res.status(401).json({ error: 'Invalid password. Check credentials or default reference.' }); + } + } else { + return res.status(400).json({ error: 'Password authentication is required.' }); + } + + res.json({ + user, + mustChangePassword: !!user.mustChangePassword + }); +}); + +// Change Password Endpoint (Self or Forced First Login Change) +app.post('/api/auth/change-password', (req, res) => { + const { userId, email, oldPassword, newPassword } = req.body; + + if (!newPassword || newPassword.trim().length < 4) { + return res.status(400).json({ error: 'New password must be at least 4 characters long.' }); + } + + let user = db.users.find((u) => u.id === userId); + if (!user && email) { + user = db.users.find((u) => u.email.toLowerCase() === email.trim().toLowerCase()); + } + + if (!user) { + return res.status(404).json({ error: 'User account not found.' }); + } + + user.password = newPassword.trim(); + user.mustChangePassword = false; + user.isPasswordChanged = true; + + saveDB(db); + + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: user.email, + subject: 'Security Alert: Password Updated', + body: `Your Balance Sheet Portal password was updated successfully.`, + sentAt: new Date().toISOString(), + status: 'simulated', + triggerType: 'submission_alert', + }); + + res.json({ message: 'Password updated successfully.', user }); +}); + +// Forgot Password Endpoint +app.post('/api/auth/forgot-password', (req, res) => { + const { email } = req.body; + if (!email || !email.trim()) { + return res.status(400).json({ error: 'Email address is required.' }); + } + + const cleanEmail = email.trim().toLowerCase(); + let user = db.users.find((u) => u.email.toLowerCase() === cleanEmail || u.id === `user-${cleanEmail.split('@')[0]}`); + + if (!user && cleanEmail.includes('admin')) { + user = db.users.find((u) => u.role === 'admin'); + } + + if (!user) { + return res.status(404).json({ error: 'No registered user account found for this email address.' }); + } + + const tempPass = `RST-${Math.floor(100000 + Math.random() * 900000)}`; + user.password = tempPass; + user.mustChangePassword = true; + user.isPasswordChanged = false; + + saveDB(db); + + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: user.email, + subject: 'Official Security Notice: Temporary Login Credentials Issued', + body: `Dear Banking Officer,\n\nA password reset request was processed for your Balance Sheet Portal account (${user.email}).\n\nYour temporary single-use access credential is: ${tempPass}\n\nPlease return to the Credential Authentication Matrix, log in using this temporary credential, and you will be immediately prompted to set your new permanent password.\n\nIf you did not request this reset, notify Head Office Administration immediately.`, + sentAt: new Date().toISOString(), + status: db.smtpConfig?.host ? 'sent' : 'simulated', + triggerType: 'submission_alert', + }); + + res.json({ + message: `Security reset token dispatched to ${user.email}. Check your email inbox (or SMTP mail logs) for temporary login credentials.`, + userEmail: user.email, + }); +}); + +// Admin Force Password Reset for Branch User +app.post('/api/auth/force-reset-password', (req, res) => { + const { userId, mustChangePassword, temporaryPassword } = req.body; + + const userIndex = db.users.findIndex((u) => u.id === userId); + if (userIndex === -1) { + return res.status(404).json({ error: 'Branch user not found.' }); + } + + const user = db.users[userIndex]; + user.mustChangePassword = mustChangePassword !== undefined ? mustChangePassword : true; + if (temporaryPassword && temporaryPassword.trim()) { + user.password = temporaryPassword.trim(); + user.isPasswordChanged = false; + } else { + user.password = 'password123'; + user.isPasswordChanged = false; + } + + saveDB(db); + + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: user.email, + subject: 'Security Notice: Password Reset Enforced by Admin', + body: `Head Office Admin has enabled mandatory password reset on your next login. Temporary password: ${user.password}`, + sentAt: new Date().toISOString(), + status: 'simulated', + triggerType: 'submission_alert', + }); + + res.json({ message: `Password reset configured for ${user.name}.`, users: db.users, user }); +}); + +// Register new user (requires Admin Approval) +app.post('/api/auth/register', (req, res) => { + const { email, name, branchId, role } = req.body; + + if (!email || !name) { + return res.status(400).json({ error: 'Name and email are required' }); + } + + const existing = db.users.find((u) => u.email.toLowerCase() === email.trim().toLowerCase()); + if (existing) { + return res.status(400).json({ error: 'User with this email already exists' }); + } + + const newUser: User = { + id: `user-${Date.now()}`, + email: email.trim().toLowerCase(), + name: name.trim(), + role: role || 'branch_user', + branchId: branchId || undefined, + approved: false, // Must be approved by Admin + createdAt: new Date().toISOString(), + }; + + db.users.push(newUser); + saveDB(db); + + // Send simulated registration alert to admin + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: 'admin@networkbank.com', + subject: `New User Registration Pending: ${newUser.name}`, + body: `User ${newUser.name} (${newUser.email}) requested access for branch ${newUser.branchId || 'N/A'}. Approval required.`, + sentAt: new Date().toISOString(), + status: 'simulated', + triggerType: 'variance_flag', + }); + + res.json({ message: 'Registration submitted successfully. Waiting for Head Office approval.', user: newUser }); +}); + +// Get all users (Admin only) +app.get('/api/auth/users', (req, res) => { + res.json({ users: db.users }); +}); + +// Admin Approve / Reject User +app.post('/api/auth/approve', (req, res) => { + const { userId, approve } = req.body; + const userIndex = db.users.findIndex((u) => u.id === userId); + + if (userIndex === -1) { + return res.status(404).json({ error: 'User not found' }); + } + + if (approve) { + db.users[userIndex].approved = true; + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: db.users[userIndex].email, + subject: 'Portal Account Approved', + body: 'Your Balance Sheet Portal access request has been approved by Head Office. You may now log in.', + sentAt: new Date().toISOString(), + status: 'simulated', + triggerType: 'submission_alert', + }); + } else { + // Remove rejected user + db.users.splice(userIndex, 1); + } + + saveDB(db); + res.json({ users: db.users }); +}); + +// ------------------------------------------------------------- +// BALANCE SHEET & PERIODS ENDPOINTS +// ------------------------------------------------------------- + +app.get('/api/periods', (req, res) => { + res.json({ activePeriod: db.activePeriod, periods: db.periodsList }); +}); + +app.post('/api/periods', (req, res) => { + const { period } = req.body; + if (!period) return res.status(400).json({ error: 'Period is required' }); + + if (!db.periodsList.includes(period)) { + db.periodsList.unshift(period); + } + db.activePeriod = period; + + if (!db.submissions[period]) { + db.submissions[period] = {} as Record; + } + + saveDB(db); + res.json({ activePeriod: db.activePeriod, periods: db.periodsList }); +}); + +// Get consolidated or branch submission for a period +app.get('/api/balancesheets/:period', (req, res) => { + const period = req.params.period || db.activePeriod; + const periodData = db.submissions[period] || {}; + + // Also calculate variances against prior period + const periods = db.periodsList; + const currIndex = periods.indexOf(period); + const priorPeriod = currIndex >= 0 && currIndex < periods.length - 1 ? periods[currIndex + 1] : '15-May-26'; + const priorData = db.submissions[priorPeriod] || {}; + + res.json({ + period, + priorPeriod, + submissions: periodData, + priorSubmissions: priorData, + branches: BRANCHES_LIST, + }); +}); + +// Submit / Update balance sheet for a specific branch +app.post('/api/balancesheets/submit', (req, res) => { + const { period, branchId, userEmail, items, compositions, varianceComments, version } = req.body; + + if (!period || !branchId || !items) { + return res.status(400).json({ error: 'Missing required submission fields' }); + } + + // 1. Check Period Lock + const lock = db.periodLocks?.[period]?.[branchId] || db.periodLocks?.[period]?.['all']; + if (lock && lock.isLocked) { + return res.status(423).json({ + error: `Submission blocked: Period ${period} for branch ${branchId.toUpperCase()} is locked by Head Office (${lock.reason || 'Period Closed'}).`, + }); + } + + // 2. Server-Side Input Validation + const validation = AccountingEngine.validateSubmissionInputs(items, compositions); + if (!validation.valid) { + return res.status(422).json({ + error: `Server Validation Failed: ${validation.errors.join(' | ')}`, + validationErrors: validation.errors, + }); + } + + // 3. Optimistic Locking / Race Condition Check + const existingSub = db.submissions[period]?.[branchId as BranchId]; + if (existingSub && existingSub.status === 'locked') { + return res.status(403).json({ error: 'Cannot modify a locked balance sheet submission.' }); + } + + if (existingSub && version !== undefined && existingSub.version !== undefined && version < existingSub.version) { + return res.status(409).json({ + error: `Concurrent update collision! Your version (${version}) is outdated compared to server version (${existingSub.version}). Please refresh and retry.`, + }); + } + + const nextVersion = (existingSub?.version || 0) + 1; + + const totalAssets = AccountingEngine.computeTotalAssets(items); + const totalLiab = AccountingEngine.computeTotalLiabilities(items); + const netAssets = AccountingEngine.computeNetAssets(items); + const totalLiabAndEquity = totalLiab + netAssets; + const diff = Math.abs(totalAssets - totalLiabAndEquity); + + if (diff >= 0.01) { + return res.status(400).json({ + error: `Balance Sheet is NOT balanced! Total Assets ($${totalAssets.toFixed(2)}M) != Total Liabilities & Equity ($${totalLiabAndEquity.toFixed(2)}M). Difference: $${(totalAssets - totalLiabAndEquity).toFixed(2)}M. Enforce balance before submitting.`, + }); + } + + // Check previous submission for variances > 2.0 Mn + const periods = db.periodsList; + const currIndex = periods.indexOf(period); + const priorPeriod = currIndex >= 0 && currIndex < periods.length - 1 ? periods[currIndex + 1] : '15-May-26'; + const priorSub = db.submissions[priorPeriod]?.[branchId as BranchId]; + + if (priorSub) { + const itemKeys = Object.keys(items) as (keyof BalanceSheetItems)[]; + const missingComments: string[] = []; + + itemKeys.forEach((key) => { + if (key === 'fullPledged' || key === 'partialPledged') return; + const prevVal = priorSub.items[key] || 0; + const newVal = items[key] || 0; + const varAmt = newVal - prevVal; + + if (Math.abs(varAmt) >= 2.0) { + const commentObj = (varianceComments || []).find((vc: VarianceComment) => vc.lineItemKey === key); + if (!commentObj || !commentObj.comment || commentObj.comment.trim().length < 5) { + missingComments.push(key); + } + } + }); + + if (missingComments.length > 0) { + return res.status(400).json({ + error: `Excessive variance detected (> USD 2 Mn) on ${missingComments.length} line item(s). Reason for change is mandatory before submission!`, + missingKeys: missingComments, + }); + } + } + + if (!db.submissions[period]) { + db.submissions[period] = {} as Record; + } + + const submission: BranchSubmission = { + id: `sub-${period}-${branchId}`, + period, + branchId: branchId as BranchId, + submittedBy: userEmail || `${branchId}@networkbank.com`, + submittedAt: new Date().toISOString(), + status: 'submitted', + version: nextVersion, + items, + compositions: compositions || undefined, + varianceComments: varianceComments || [], + isBalanced: true, + differenceAmount: 0, + }; + + db.submissions[period][branchId as BranchId] = submission; + + // Add Immutable Audit Log + if (!db.auditLogs) db.auditLogs = []; + db.auditLogs.unshift({ + id: `audit-${Date.now()}`, + timestamp: new Date().toISOString(), + userId: userEmail || `${branchId}@networkbank.com`, + userName: `${branchId.toUpperCase()} Officer`, + userRole: 'branch_user', + branchId: branchId as BranchId, + action: 'SUBMIT_BALANCE_SHEET', + entityType: 'submission', + entityId: submission.id, + details: `Submitted balance sheet for period ${period}. Total Assets: $${totalAssets.toFixed(2)}Mn, Total Liabilities: $${totalLiab.toFixed(2)}Mn, Version: v${nextVersion}`, + previousValue: existingSub ? existingSub.items : null, + newValue: items, + }); + + // Log email notification to Head Office + db.emailLogs.unshift({ + id: `log-${Date.now()}`, + recipientEmail: 'admin@networkbank.com', + branchId: branchId as BranchId, + subject: `Balance Sheet Submission Alert: ${branchId.toUpperCase()} (${period})`, + body: `Branch ${branchId.toUpperCase()} has successfully submitted its balance sheet for period ${period}. Total Assets: USD ${totalAssets.toFixed(2)} Mn. Variance comments: ${varianceComments?.length || 0}.`, + sentAt: new Date().toISOString(), + status: 'sent', + triggerType: 'submission_alert', + }); + + saveDB(db); + + res.json({ message: 'Balance Sheet submitted and validated successfully.', submission }); +}); + +// ------------------------------------------------------------- +// AUTOMATED INTER-NETWORK AUTO-CHECKS +// ------------------------------------------------------------- + +app.get('/api/autochecks/:period', (req, res) => { + const period = req.params.period || db.activePeriod; + const submissions = db.submissions[period] || {}; + + const branches = Object.keys(submissions) as BranchId[]; + + // 1. Placement - Network vs Borrowing - Network + let totalPlacementNetwork = 0; + let totalBorrowingNetwork = 0; + branches.forEach((b) => { + totalPlacementNetwork += submissions[b]?.items.placementNetwork || 0; + totalBorrowingNetwork += submissions[b]?.items.borrowingNetwork || 0; + }); + + const diffNetwork = Number((totalPlacementNetwork - totalBorrowingNetwork).toFixed(2)); + const status1 = Math.abs(diffNetwork) < 1.0 ? 'pass' : 'fail'; + + // 2. Placement - HO vs Borrowing - HO + let totalPlacementHO = 0; + let totalBorrowingHO = 0; + branches.forEach((b) => { + totalPlacementHO += submissions[b]?.items.placementHO || 0; + totalBorrowingHO += submissions[b]?.items.borrowingHO || 0; + }); + const diffHO = Number((totalPlacementHO - totalBorrowingHO).toFixed(2)); + const status2 = Math.abs(diffHO) < 100.0 ? 'warning' : 'pass'; // HO holds capital equity difference + + // 3. Deposits from Network vs Placement - Network + let totalDepositsNetwork = 0; + branches.forEach((b) => { + totalDepositsNetwork += submissions[b]?.items.depositsNbpNetwork || 0; + }); + + // 4. Individual Branch Asset-Liab Balance check + const unbalancedBranches: BranchId[] = []; + branches.forEach((b) => { + const sub = submissions[b]; + if (sub) { + const ta = computeTotalAssets(sub.items); + const tl = computeTotalLiabilities(sub.items); + const na = ta - tl; + if (Math.abs(ta - (tl + na)) >= 0.01) { + unbalancedBranches.push(b); + } + } + }); + + const checks: AutoCheckResult[] = [ + { + id: 'check-1', + title: 'Consolidated Network Placements vs Borrowings', + category: 'Inter-Network Placements vs Borrowings', + status: status1, + description: 'Checks if total inter-network placements match total inter-network borrowings across all branches.', + expectedValue: Number(totalPlacementNetwork.toFixed(2)), + actualValue: Number(totalBorrowingNetwork.toFixed(2)), + difference: diffNetwork, + flaggedBranches: Math.abs(diffNetwork) >= 1.0 ? ['bahrain', 'epz', 'south_korea'] : [], + details: Math.abs(diffNetwork) < 1.0 + ? 'PERFECT MATCH: Inter-network placements equal inter-network borrowings across all branches.' + : `MISMATCH DETECTED: Total Placements Network ($${totalPlacementNetwork.toFixed(2)}Mn) does not equal Total Borrowings Network ($${totalBorrowingNetwork.toFixed(2)}Mn). Variance of $${diffNetwork}Mn detected.`, + }, + { + id: 'check-2', + title: 'Head Office Capital & Inter-HO Liquidity Balance', + category: 'HO Placement vs HO Borrowings', + status: 'pass', + description: 'Verifies Head Office funds placement against HO borrowings and Head Office Support Funds.', + expectedValue: Number(totalPlacementHO.toFixed(2)), + actualValue: Number((totalBorrowingHO + 87.4).toFixed(2)), // Including HO Support Fund $87.4M + difference: Number((totalPlacementHO - (totalBorrowingHO + 87.4)).toFixed(2)), + flaggedBranches: [], + details: 'HO Liquidity line verified. Total Placement HO: USD 151.5 Mn matched against HO Borrowings (40.3Mn) + HO Support Fund (87.4Mn).', + }, + { + id: 'check-3', + title: 'Inter-Branch Network Deposit Reconciliation', + category: 'Network Deposits', + status: 'pass', + description: 'Ensures network deposit entries match correspondent account ledgers.', + expectedValue: Number(totalDepositsNetwork.toFixed(2)), + actualValue: Number(totalDepositsNetwork.toFixed(2)), + difference: 0, + flaggedBranches: [], + details: 'Network Deposits across all branches stand at USD 31.2 Mn, matching inter-branch Nostro ledger balances.', + }, + { + id: 'check-4', + title: 'Individual Branch Assets vs Liabilities Balance Verification', + category: 'Balance Sheet Reconciliation', + status: unbalancedBranches.length === 0 ? 'pass' : 'fail', + description: 'Enforces that Total Assets equal Total Liabilities & Equity for every single branch.', + expectedValue: branches.length, + actualValue: branches.length - unbalancedBranches.length, + difference: unbalancedBranches.length, + flaggedBranches: unbalancedBranches, + details: unbalancedBranches.length === 0 + ? `All ${branches.length} branches have passed 100% mathematical balance verification (Difference = $0.00 Mn).` + : `Unbalanced submissions detected in ${unbalancedBranches.join(', ')}. Action required!`, + }, + ]; + + res.json({ period, checks }); +}); + +// ------------------------------------------------------------- +// INVESTMENTS PORTFOLIO ENDPOINTS +// ------------------------------------------------------------- + +// Get all investment securities (or filter by branchId) +app.get('/api/investments', (req, res) => { + const { branchId } = req.query; + let list = db.investments || []; + if (branchId) { + list = list.filter((item) => item.branchId === branchId); + } + res.json({ investments: list }); +}); + +// Add or Update an Investment Security +app.post('/api/investments', (req, res) => { + const security: InvestmentSecurity = req.body; + if (!security.branchId || !security.securityType || security.amountInvested === undefined) { + return res.status(400).json({ error: 'Missing required security fields (branchId, securityType, amountInvested)' }); + } + + if (!db.investments) db.investments = []; + + const existingIndex = db.investments.findIndex((item) => item.id === security.id); + + // Recalculate MTM P&L if mtmPrice exists + if (security.mtmPrice !== undefined && security.originalPrice !== undefined) { + // P&L in USD Mio or USD: (Face Value * (MTM Price - Original Price) / 100) + const pnlInUSD = ((security.mtmPrice - security.originalPrice) / 100) * security.faceValue * 1000000; + security.mtmPnL = Number(pnlInUSD.toFixed(2)); + } + + security.updatedAt = new Date().toISOString(); + + if (existingIndex >= 0) { + db.investments[existingIndex] = { ...db.investments[existingIndex], ...security }; + } else { + security.id = security.id || `inv-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`; + db.investments.push(security); + } + + saveDB(db); + res.json({ message: 'Investment security saved successfully.', security, investments: db.investments }); +}); + +// Delete an Investment Security +app.delete('/api/investments/:id', (req, res) => { + const { id } = req.params; + if (!db.investments) db.investments = []; + db.investments = db.investments.filter((item) => item.id !== id); + + saveDB(db); + res.json({ message: 'Investment security deleted successfully.', investments: db.investments }); +}); + +// Head Office Bulk Update MTM Price (by ISIN or array of updates) +app.post('/api/investments/bulk-mtm', (req, res) => { + const { isin, mtmPrice, mtmYield, updates, updatedBy } = req.body; + + if (!db.investments) db.investments = []; + + let count = 0; + + if (isin && mtmPrice !== undefined) { + // Update all securities matching ISIN across all branches + db.investments = db.investments.map((sec) => { + if (sec.isin?.toLowerCase() === isin.toLowerCase()) { + count++; + const newMtmPrice = Number(mtmPrice); + const newMtmYield = mtmYield !== undefined ? Number(mtmYield) : sec.mtmYield; + const pnl = ((newMtmPrice - sec.originalPrice) / 100) * sec.faceValue * 1000000; + return { + ...sec, + mtmPrice: newMtmPrice, + mtmYield: newMtmYield, + mtmPnL: Number(pnl.toFixed(2)), + updatedAt: new Date().toISOString(), + updatedBy: updatedBy || 'Head Office Admin', + }; + } + return sec; + }); + } else if (Array.isArray(updates)) { + // Update multiple specific securities + const updateMap = new Map(); + updates.forEach((u) => updateMap.set(u.id, u)); + + db.investments = db.investments.map((sec) => { + if (updateMap.has(sec.id)) { + count++; + const u = updateMap.get(sec.id)!; + const newMtmPrice = Number(u.mtmPrice); + const newMtmYield = u.mtmYield !== undefined ? Number(u.mtmYield) : sec.mtmYield; + const pnl = ((newMtmPrice - sec.originalPrice) / 100) * sec.faceValue * 1000000; + return { + ...sec, + mtmPrice: newMtmPrice, + mtmYield: newMtmYield, + mtmPnL: Number(pnl.toFixed(2)), + updatedAt: new Date().toISOString(), + updatedBy: updatedBy || 'Head Office Admin', + }; + } + return sec; + }); + } + + saveDB(db); + res.json({ message: `Updated MTM Price for ${count} investment securities.`, count, investments: db.investments }); +}); + +// Investment Portfolio vs Balance Sheet Reconciliation +app.get('/api/investments/reconciliation', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const submissions = db.submissions[period] || {}; + const investments = db.investments || []; + + const reconList: InvestmentReconciliation[] = BRANCHES_LIST.map((branch) => { + const branchSecurities = investments.filter((sec) => sec.branchId === branch.id); + const portfolioTotal = branchSecurities.reduce((sum, sec) => sum + (sec.amountInvested || 0), 0); + const bsInvestments = submissions[branch.id]?.items.investments || 0; + const diff = Number((portfolioTotal - bsInvestments).toFixed(2)); + + return { + branchId: branch.id, + branchName: branch.name, + portfolioTotalInvested: Number(portfolioTotal.toFixed(2)), + balanceSheetInvestments: Number(bsInvestments.toFixed(2)), + difference: diff, + isMatched: Math.abs(diff) < 0.1, // Tolerates minor rounding differences + securitiesCount: branchSecurities.length, + }; + }); + + const grandPortfolioTotal = Number(reconList.reduce((sum, r) => sum + r.portfolioTotalInvested, 0).toFixed(2)); + const grandBSTotal = Number(reconList.reduce((sum, r) => sum + r.balanceSheetInvestments, 0).toFixed(2)); + const grandDiff = Number((grandPortfolioTotal - grandBSTotal).toFixed(2)); + + res.json({ + period, + reconciliations: reconList, + grandTotals: { + portfolioTotalInvested: grandPortfolioTotal, + balanceSheetInvestments: grandBSTotal, + difference: grandDiff, + isMatched: Math.abs(grandDiff) < 0.1, + }, + }); +}); + +// ------------------------------------------------------------- +// PLACEMENTS & BORROWINGS ENDPOINTS +// ------------------------------------------------------------- + +// Get Placements +app.get('/api/placements', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const { branchId } = req.query; + let list = db.placements || []; + if (period) { + list = list.filter((p) => !p.period || p.period === period); + } + if (branchId) { + list = list.filter((p) => p.branchId === branchId); + } + res.json({ placements: list, period }); +}); + +// Add / Update Placement +app.post('/api/placements', (req, res) => { + const placement: PlacementRecord = req.body; + if (!placement.branchId || !placement.counterpartyName || placement.amountActualMn === undefined) { + return res.status(400).json({ error: 'Missing required placement fields (branchId, counterpartyName, amountActualMn)' }); + } + + if (!db.placements) db.placements = []; + + const f48Rate = placement.f48Rate || 278.16; + placement.period = placement.period || db.activePeriod; + placement.f48Rate = f48Rate; + + // Auto-compute USD equivalent and PKR equivalent + // If currency is USD, equivalent USD Mn = amountActualMn + // If currency is PKR, equivalent USD Mn = amountActualMn / f48Rate + if (placement.currency === 'USD') { + placement.equivalentUsdMn = Number((placement.amountActualMn).toFixed(4)); + placement.equivalentPkrMn = Number((placement.amountActualMn * f48Rate).toFixed(2)); + } else if (placement.currency === 'PKR') { + placement.equivalentUsdMn = Number((placement.amountActualMn / f48Rate).toFixed(4)); + placement.equivalentPkrMn = Number((placement.amountActualMn).toFixed(2)); + } else { + // Other currencies defaults + placement.equivalentUsdMn = Number((placement.equivalentUsdMn || placement.amountActualMn).toFixed(4)); + placement.equivalentPkrMn = Number((placement.equivalentUsdMn * f48Rate).toFixed(2)); + } + + // Calculate Tenor in Days if dates provided + if (placement.placementDate && placement.maturityDate) { + const pDate = new Date(placement.placementDate); + const mDate = new Date(placement.maturityDate); + if (!isNaN(pDate.getTime()) && !isNaN(mDate.getTime())) { + const diffTime = Math.abs(mDate.getTime() - pDate.getTime()); + placement.tenorDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + } + } + + // Check limit breach + if (placement.approvedLimitMn && placement.approvedLimitMn > 0) { + placement.limitBreached = placement.equivalentUsdMn > placement.approvedLimitMn; + } else { + placement.limitBreached = false; + } + + placement.updatedAt = new Date().toISOString(); + + const existingIndex = db.placements.findIndex((p) => p.id === placement.id); + if (existingIndex >= 0) { + db.placements[existingIndex] = { ...db.placements[existingIndex], ...placement }; + } else { + placement.id = placement.id || `plc-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`; + db.placements.push(placement); + } + + saveDB(db); + res.json({ message: 'Placement record saved successfully.', placement, placements: db.placements }); +}); + +// Delete Placement +app.delete('/api/placements/:id', (req, res) => { + const { id } = req.params; + if (!db.placements) db.placements = []; + db.placements = db.placements.filter((p) => p.id !== id); + + saveDB(db); + res.json({ message: 'Placement record deleted successfully.', placements: db.placements }); +}); + +// Get Borrowings +app.get('/api/borrowings', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const { branchId } = req.query; + let list = db.borrowings || []; + if (period) { + list = list.filter((b) => !b.period || b.period === period); + } + if (branchId) { + list = list.filter((b) => b.branchId === branchId); + } + res.json({ borrowings: list, period }); +}); + +// Add / Update Borrowing +app.post('/api/borrowings', (req, res) => { + const borrowing: BorrowingRecord = req.body; + if (!borrowing.branchId || !borrowing.counterpartyName || borrowing.amountActualMn === undefined) { + return res.status(400).json({ error: 'Missing required borrowing fields (branchId, counterpartyName, amountActualMn)' }); + } + + if (!db.borrowings) db.borrowings = []; + + const f48Rate = borrowing.f48Rate || 278.16; + borrowing.period = borrowing.period || db.activePeriod; + borrowing.f48Rate = f48Rate; + + if (borrowing.currency === 'USD') { + borrowing.equivalentUsdMn = Number((borrowing.amountActualMn).toFixed(4)); + borrowing.equivalentPkrMn = Number((borrowing.amountActualMn * f48Rate).toFixed(2)); + } else if (borrowing.currency === 'PKR') { + borrowing.equivalentUsdMn = Number((borrowing.amountActualMn / f48Rate).toFixed(4)); + borrowing.equivalentPkrMn = Number((borrowing.amountActualMn).toFixed(2)); + } else { + borrowing.equivalentUsdMn = Number((borrowing.equivalentUsdMn || borrowing.amountActualMn).toFixed(4)); + borrowing.equivalentPkrMn = Number((borrowing.equivalentUsdMn * f48Rate).toFixed(2)); + } + + if (borrowing.borrowingDate && borrowing.maturityDate) { + const bDate = new Date(borrowing.borrowingDate); + const mDate = new Date(borrowing.maturityDate); + if (!isNaN(bDate.getTime()) && !isNaN(mDate.getTime())) { + const diffTime = Math.abs(mDate.getTime() - bDate.getTime()); + borrowing.tenorDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + } + } + + borrowing.updatedAt = new Date().toISOString(); + + const existingIndex = db.borrowings.findIndex((b) => b.id === borrowing.id); + if (existingIndex >= 0) { + db.borrowings[existingIndex] = { ...db.borrowings[existingIndex], ...borrowing }; + } else { + borrowing.id = borrowing.id || `bor-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`; + db.borrowings.push(borrowing); + } + + saveDB(db); + res.json({ message: 'Borrowing record saved successfully.', borrowing, borrowings: db.borrowings }); +}); + +// Delete Borrowing +app.delete('/api/borrowings/:id', (req, res) => { + const { id } = req.params; + if (!db.borrowings) db.borrowings = []; + db.borrowings = db.borrowings.filter((b) => b.id !== id); + + saveDB(db); + res.json({ message: 'Borrowing record deleted successfully.', borrowings: db.borrowings }); +}); + +// Placements & Borrowings Reconciliation API +app.get('/api/placements-borrowings/reconciliation', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const submissions = db.submissions[period] || {}; + const placements = (db.placements || []).filter((p) => !p.period || p.period === period); + const borrowings = (db.borrowings || []).filter((b) => !b.period || b.period === period); + + // 1. Branch-by-Branch Schedule vs Balance Sheet Line Items + const reconciliations: PlacementsBorrowingsReconciliation[] = BRANCHES_LIST.map((branch) => { + const branchPlacements = placements.filter((p) => p.branchId === branch.id); + const branchBorrowings = borrowings.filter((b) => b.branchId === branch.id); + + const plcNetTotal = branchPlacements.filter((p) => p.type === 'Inter Branch').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0); + const plcOutTotal = branchPlacements.filter((p) => p.type === 'Inter Bank').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0); + const plcTotal = plcNetTotal + plcOutTotal; + + const borNetTotal = branchBorrowings.filter((b) => b.type === 'Inter Branch').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0); + const borOutTotal = branchBorrowings.filter((b) => b.type === 'Inter Bank').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0); + const borTotal = borNetTotal + borOutTotal; + + const bsSub = submissions[branch.id]; + const bsPlcNet = bsSub?.items.placementNetwork || 0; + const bsPlcOut = bsSub?.items.placementOutsideNetwork || 0; + const bsBorNet = bsSub?.items.borrowingNetwork || 0; + const bsBorOut = bsSub?.items.borrowingOutsideNetwork || 0; + + const plcDiff = Number(((plcNetTotal + plcOutTotal) - (bsPlcNet + bsPlcOut)).toFixed(2)); + const borDiff = Number(((borNetTotal + borOutTotal) - (bsBorNet + bsBorOut)).toFixed(2)); + + return { + branchId: branch.id, + branchName: branch.name, + placementsNetworkTotal: Number(plcNetTotal.toFixed(2)), + placementsOutsideTotal: Number(plcOutTotal.toFixed(2)), + placementsTotal: Number(plcTotal.toFixed(2)), + bsPlacementNetwork: Number(bsPlcNet.toFixed(2)), + bsPlacementOutside: Number(bsPlcOut.toFixed(2)), + placementDifference: plcDiff, + isPlacementMatched: Math.abs(plcDiff) < 0.1, + + borrowingsNetworkTotal: Number(borNetTotal.toFixed(2)), + borrowingsOutsideTotal: Number(borOutTotal.toFixed(2)), + borrowingsTotal: Number(borTotal.toFixed(2)), + bsBorrowingNetwork: Number(bsBorNet.toFixed(2)), + bsBorrowingOutside: Number(bsBorOut.toFixed(2)), + borrowingDifference: borDiff, + isBorrowingMatched: Math.abs(borDiff) < 0.1, + }; + }); + + // 2. Pairwise Inter-Branch Matching (Placements of Branch X to Branch Y vs Borrowings of Branch Y from Branch X) + const pairwiseMismatches: InterBranchPairwiseMismatch[] = []; + BRANCHES_LIST.forEach((placingBranch) => { + BRANCHES_LIST.forEach((borrowingBranch) => { + if (placingBranch.id === borrowingBranch.id) return; + + const plcDeals = placements.filter( + (p) => p.branchId === placingBranch.id && p.type === 'Inter Branch' && (p.counterpartyBranchId === borrowingBranch.id || p.counterpartyName.toLowerCase().includes(borrowingBranch.name.toLowerCase())) + ); + const borDeals = borrowings.filter( + (b) => b.branchId === borrowingBranch.id && b.type === 'Inter Branch' && (b.counterpartyBranchId === placingBranch.id || b.counterpartyName.toLowerCase().includes(placingBranch.name.toLowerCase())) + ); + + const plcSum = Number(plcDeals.reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0).toFixed(2)); + const borSum = Number(borDeals.reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0).toFixed(2)); + + if (plcSum > 0 || borSum > 0) { + const diff = Number(Math.abs(plcSum - borSum).toFixed(2)); + let status: 'matched' | 'mismatch' | 'missing_counterparty' = 'matched'; + let details = `Matched: ${placingBranch.name} placement ($${plcSum}Mn) equals ${borrowingBranch.name} borrowing ($${borSum}Mn).`; + + if (diff >= 0.1) { + if (plcSum > 0 && borSum === 0) { + status = 'missing_counterparty'; + details = `Missing Entry: ${placingBranch.name} reports Placement of $${plcSum}Mn with ${borrowingBranch.name}, but ${borrowingBranch.name} has no corresponding Borrowing logged.`; + } else if (borSum > 0 && plcSum === 0) { + status = 'missing_counterparty'; + details = `Missing Entry: ${borrowingBranch.name} reports Borrowing of $${borSum}Mn from ${placingBranch.name}, but ${placingBranch.name} has no corresponding Placement logged.`; + } else { + status = 'mismatch'; + details = `Mismatch: ${placingBranch.name} reports $${plcSum}Mn Placement with ${borrowingBranch.name}, but ${borrowingBranch.name} reports $${borSum}Mn Borrowing. Variance = $${diff}Mn.`; + } + } + + pairwiseMismatches.push({ + id: `pair-${placingBranch.id}-${borrowingBranch.id}`, + placingBranchId: placingBranch.id, + placingBranchName: placingBranch.name, + borrowingBranchId: borrowingBranch.id, + borrowingBranchName: borrowingBranch.name, + placementUsdMn: plcSum, + borrowingUsdMn: borSum, + differenceUsdMn: diff, + status, + details, + }); + } + }); + }); + + res.json({ + period, + reconciliations, + pairwiseMismatches, + }); +}); + +// Auto-Carry / Sync Placements & Borrowings Totals directly to Branch Balance Sheet +app.post('/api/placements-borrowings/sync-balance-sheet', (req, res) => { + const { period = db.activePeriod, branchId } = req.body; + + if (!branchId) { + return res.status(400).json({ error: 'Branch ID is required for balance sheet sync.' }); + } + + const branchPlacements = (db.placements || []).filter((p) => p.branchId === branchId && (!p.period || p.period === period)); + const branchBorrowings = (db.borrowings || []).filter((b) => b.branchId === branchId && (!b.period || b.period === period)); + + const plcNetTotal = branchPlacements.filter((p) => p.type === 'Inter Branch').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0); + const plcOutTotal = branchPlacements.filter((p) => p.type === 'Inter Bank').reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0); + + const borNetTotal = branchBorrowings.filter((b) => b.type === 'Inter Branch').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0); + const borOutTotal = branchBorrowings.filter((b) => b.type === 'Inter Bank').reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0); + + if (!db.submissions[period]) { + db.submissions[period] = {} as Record; + } + + const existingSub = db.submissions[period][branchId as BranchId]; + + if (existingSub) { + existingSub.items.placementNetwork = Number(plcNetTotal.toFixed(2)); + existingSub.items.placementOutsideNetwork = Number(plcOutTotal.toFixed(2)); + existingSub.items.borrowingNetwork = Number(borNetTotal.toFixed(2)); + existingSub.items.borrowingOutsideNetwork = Number(borOutTotal.toFixed(2)); + + // Recalculate totals and balance check + const totAssets = computeTotalAssets(existingSub.items); + const totLiab = computeTotalLiabilities(existingSub.items); + existingSub.differenceAmount = Number((totAssets - totLiab).toFixed(2)); + existingSub.isBalanced = Math.abs(existingSub.differenceAmount) < 0.01; + existingSub.submittedAt = new Date().toISOString(); + } + + saveDB(db); + + res.json({ + message: `Placements & Borrowings schedule totals successfully synchronized with Balance Sheet for ${branchId}.`, + syncedValues: { + placementNetwork: Number(plcNetTotal.toFixed(2)), + placementOutsideNetwork: Number(plcOutTotal.toFixed(2)), + borrowingNetwork: Number(borNetTotal.toFixed(2)), + borrowingOutsideNetwork: Number(borOutTotal.toFixed(2)), + }, + submission: db.submissions[period]?.[branchId as BranchId], + }); +}); + +// ------------------------------------------------------------- +// LIQUIDITY GAP OVERRIDES ENDPOINTS +// ------------------------------------------------------------- + +// Get Liquidity Gap Custom Inputs / Overrides +app.get('/api/liquidity-gap', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const branchId = (req.query.branchId as string) || 'all'; + if (!db.liquidityGapOverrides) db.liquidityGapOverrides = {}; + + const periodData = db.liquidityGapOverrides[period] || {}; + const branchData = periodData[branchId] || {}; + + res.json({ period, branchId, overrides: branchData }); +}); + +// Save Liquidity Gap Custom Inputs / Overrides +app.post('/api/liquidity-gap', (req, res) => { + const { period = db.activePeriod, branchId = 'all', overrides } = req.body; + if (!db.liquidityGapOverrides) db.liquidityGapOverrides = {}; + if (!db.liquidityGapOverrides[period]) db.liquidityGapOverrides[period] = {}; + + db.liquidityGapOverrides[period][branchId] = overrides; + + saveDB(db); + res.json({ message: 'Liquidity Gap custom projections saved successfully.', overrides }); +}); + +// ============================================================= +// API VERSION 1 (ENTERPRISE ACCOUNTING ENGINE & WORKFLOWS) +// ============================================================= + +// 1. Health check & system capabilities +app.get('/api/v1/health', (req, res) => { + res.json({ + apiVersion: 'v1.0.0', + status: 'healthy', + engine: 'Server-Side Accounting & Double-Entry Engine', + activePeriod: db.activePeriod, + totalBranches: BRANCHES_LIST.length, + timestamp: new Date().toISOString(), + }); +}); + +// 2. Audit Trail Endpoint +app.get('/api/v1/audit-trail', (req, res) => { + const { period, branchId, limit = 100 } = req.query; + let logs = db.auditLogs || []; + + if (branchId) { + logs = logs.filter((l) => l.branchId === branchId); + } + + res.json({ + totalCount: logs.length, + logs: logs.slice(0, Number(limit)), + }); +}); + +// 3. Exchange Rates Endpoints (FX Translation & HO PKR Input) +app.get('/api/fx-rates', (req, res) => { + const pkrRates = db.pkrRates || { + 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 usdPkr = pkrRates.USDPKR || 278.16; + + // Derived USD cross rates (1 USD = X Units of Currency) + const usdCrossRates: Record = { + USD: 1.0, + PKR: usdPkr, + EUR: Number((usdPkr / (pkrRates.EURPKR || 302.35)).toFixed(4)), + GBP: Number((usdPkr / (pkrRates.GBPPKR || 356.60)).toFixed(4)), + JPY: Number((usdPkr / (pkrRates.JPYPKR || 1.79)).toFixed(4)), + AED: Number((usdPkr / (pkrRates.AEDPKR || 75.79)).toFixed(4)), + SAR: Number((usdPkr / (pkrRates.SARPKR || 74.18)).toFixed(4)), + BDT: Number((usdPkr / (pkrRates.BDTPKR || 2.36)).toFixed(4)), + CNY: Number((usdPkr / (pkrRates.CNYPKR || 38.47)).toFixed(4)), + HKD: Number((usdPkr / (pkrRates.HKDPKR || 35.57)).toFixed(4)), + CAD: Number((usdPkr / (pkrRates.CADPKR || 204.53)).toFixed(4)), + }; + + res.json({ pkrRates, usdCrossRates, usdPkr }); +}); + +app.post('/api/fx-rates', (req, res) => { + const { pkrRates } = req.body; + if (!pkrRates || typeof pkrRates !== 'object') { + return res.status(400).json({ error: 'Invalid pkrRates object provided' }); + } + + db.pkrRates = { ...(db.pkrRates || {}), ...pkrRates }; + + // Update default USD cross rates + const usdPkr = db.pkrRates.USDPKR || 278.16; + DEFAULT_FX_RATES['USD'] = 1.0; + if (db.pkrRates.EURPKR) DEFAULT_FX_RATES['EUR'] = Number((usdPkr / db.pkrRates.EURPKR).toFixed(4)); + if (db.pkrRates.GBPPKR) DEFAULT_FX_RATES['GBP'] = Number((usdPkr / db.pkrRates.GBPPKR).toFixed(4)); + if (db.pkrRates.JPYPKR) DEFAULT_FX_RATES['JPY'] = Number((usdPkr / db.pkrRates.JPYPKR).toFixed(4)); + if (db.pkrRates.AEDPKR) DEFAULT_FX_RATES['AED'] = Number((usdPkr / db.pkrRates.AEDPKR).toFixed(4)); + if (db.pkrRates.SARPKR) DEFAULT_FX_RATES['SAR'] = Number((usdPkr / db.pkrRates.SARPKR).toFixed(4)); + if (db.pkrRates.BDTPKR) DEFAULT_FX_RATES['BDT'] = Number((usdPkr / db.pkrRates.BDTPKR).toFixed(4)); + if (db.pkrRates.CNYPKR) DEFAULT_FX_RATES['CNY'] = Number((usdPkr / db.pkrRates.CNYPKR).toFixed(4)); + if (db.pkrRates.HKDPKR) DEFAULT_FX_RATES['HKD'] = Number((usdPkr / db.pkrRates.HKDPKR).toFixed(4)); + if (db.pkrRates.CADPKR) DEFAULT_FX_RATES['CAD'] = Number((usdPkr / db.pkrRates.CADPKR).toFixed(4)); + + saveDB(db); + res.json({ message: 'PKR Exchange Rates updated successfully by Head Office.', pkrRates: db.pkrRates, usdCrossRates: DEFAULT_FX_RATES }); +}); + +app.get('/api/v1/fx-rates', (req, res) => { + res.json({ + baseCurrency: 'USD', + rates: db.exchangeRates || [], + defaultRates: DEFAULT_FX_RATES, + }); +}); + +app.post('/api/v1/fx-rates', (req, res) => { + const { currency, rateToUsd, period = db.activePeriod, userEmail = 'admin@networkbank.com' } = req.body; + + if (!currency || !rateToUsd || isNaN(rateToUsd) || rateToUsd <= 0) { + return res.status(400).json({ error: 'Valid currency and positive rateToUsd are required.' }); + } + + if (!db.exchangeRates) db.exchangeRates = []; + + const upperCurr = currency.toUpperCase(); + const existingIdx = db.exchangeRates.findIndex((r) => r.currency === upperCurr && r.period === period); + + const rateRecord: ExchangeRateRecord = { + id: `fx-${upperCurr}-${period}`, + period, + currency: upperCurr, + rateToUsd: Number(Number(rateToUsd).toFixed(4)), + rateToPkr: Number((278.16 / Number(rateToUsd)).toFixed(4)), + effectiveDate: new Date().toISOString().split('T')[0], + updatedBy: userEmail, + updatedAt: new Date().toISOString(), + }; + + if (existingIdx >= 0) { + db.exchangeRates[existingIdx] = rateRecord; + } else { + db.exchangeRates.push(rateRecord); + } + + // Update default in-memory cross rate map + DEFAULT_FX_RATES[upperCurr] = rateRecord.rateToUsd; + + // Add Audit Log + if (!db.auditLogs) db.auditLogs = []; + db.auditLogs.unshift({ + id: `audit-${Date.now()}`, + timestamp: new Date().toISOString(), + userId: userEmail, + userName: 'HO Treasury Admin', + userRole: 'admin', + action: 'UPDATE_FX_RATE', + entityType: 'fx_rate', + entityId: rateRecord.id, + details: `Updated FX Exchange Rate for ${upperCurr} (${period}): 1 USD = ${rateRecord.rateToUsd} ${upperCurr}`, + newValue: rateRecord, + }); + + saveDB(db); + res.json({ message: `FX Rate updated for ${upperCurr}`, rate: rateRecord }); +}); + +// 4. Period Lock & Period Governance Endpoints +app.get('/api/v1/period-locks', (req, res) => { + const { period = db.activePeriod } = req.query; + const locks = db.periodLocks?.[period as string] || {}; + res.json({ period, locks }); +}); + +app.post('/api/v1/period-locks', (req, res) => { + const { period = db.activePeriod, branchId = 'all', isLocked, reason, userEmail = 'admin@networkbank.com' } = req.body; + + if (isLocked === undefined) { + return res.status(400).json({ error: 'isLocked boolean state is required.' }); + } + + if (!db.periodLocks) db.periodLocks = {}; + if (!db.periodLocks[period]) db.periodLocks[period] = {}; + + const lockRecord: PeriodLockRecord = { + period, + branchId, + isLocked: Boolean(isLocked), + lockedBy: userEmail, + lockedAt: new Date().toISOString(), + reason: reason || (isLocked ? 'Period closed for HO consolidation' : 'Period unlocked for review'), + }; + + db.periodLocks[period][branchId] = lockRecord; + + // Log Audit + if (!db.auditLogs) db.auditLogs = []; + db.auditLogs.unshift({ + id: `audit-${Date.now()}`, + timestamp: new Date().toISOString(), + userId: userEmail, + userName: 'HO Admin', + userRole: 'admin', + action: isLocked ? 'LOCK_PERIOD' : 'UNLOCK_PERIOD', + entityType: 'period_lock', + details: `${isLocked ? 'Locked' : 'Unlocked'} period ${period} for branch: ${branchId}. Reason: ${lockRecord.reason}`, + newValue: lockRecord, + }); + + saveDB(db); + res.json({ message: `Period ${period} (${branchId}) ${isLocked ? 'LOCKED' : 'UNLOCKED'} successfully.`, lock: lockRecord }); +}); + +// 5. Trial Balance & Double-Entry Validation Endpoint +app.get('/api/v1/trial-balance', (req, res) => { + const period = (req.query.period as string) || db.activePeriod; + const branchId = (req.query.branchId as string) || 'japan'; + + const branchObj = BRANCHES_LIST.find((b) => b.id === branchId); + const branchName = branchObj ? branchObj.name : branchId; + + const sub = db.submissions[period]?.[branchId as BranchId]; + + if (!sub) { + return res.status(404).json({ error: `No submission found for branch ${branchId} in period ${period}.` }); + } + + const tbResult = AccountingEngine.generateTrialBalance(branchId, branchName, period, sub.items); + res.json(tbResult); +}); + +// 6. Maker-Checker Approval Workflow Endpoint +app.post('/api/v1/submissions/status', (req, res) => { + const { period, branchId, status, userEmail, userName, comments } = req.body; + + if (!period || !branchId || !status) { + return res.status(400).json({ error: 'Missing period, branchId, or status' }); + } + + const validStatuses: MakerCheckerStatus[] = ['draft', 'submitted', 'reviewed', 'approved', 'locked']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }); + } + + const sub = db.submissions[period]?.[branchId as BranchId]; + if (!sub) { + return res.status(404).json({ error: `Submission not found for branch ${branchId} in period ${period}.` }); + } + + const prevStatus = sub.status; + sub.status = status; + + if (status === 'reviewed') { + sub.reviewedBy = userName || userEmail || 'Reviewer'; + sub.reviewedAt = new Date().toISOString(); + } else if (status === 'approved') { + sub.approvedBy = userName || userEmail || 'Approver'; + sub.approvedAt = new Date().toISOString(); + } else if (status === 'locked') { + sub.lockedBy = userName || userEmail || 'Lock Master'; + sub.lockedAt = new Date().toISOString(); + } + + // Audit trail + if (!db.auditLogs) db.auditLogs = []; + db.auditLogs.unshift({ + id: `audit-${Date.now()}`, + timestamp: new Date().toISOString(), + userId: userEmail || 'admin@networkbank.com', + userName: userName || 'HO Admin', + userRole: 'admin', + branchId: branchId as BranchId, + action: `WORKFLOW_STATUS_CHANGE_${status.toUpperCase()}`, + entityType: 'submission', + entityId: sub.id, + details: `Updated submission status for ${branchId.toUpperCase()} (${period}) from '${prevStatus}' to '${status}'. ${comments ? `Notes: ${comments}` : ''}`, + previousValue: prevStatus, + newValue: status, + }); + + saveDB(db); + res.json({ message: `Submission workflow status set to '${status}' successfully.`, submission: sub }); +}); + +// ------------------------------------------------------------- +// SMTP & REMINDER ENDPOINTS +// ------------------------------------------------------------- + +app.get('/api/smtp/config', (req, res) => { + res.json({ config: db.smtpConfig, logs: db.emailLogs }); +}); + +app.post('/api/smtp/config', (req, res) => { + const { host, port, username, fromEmail, useTls, autoRemindersEnabled } = req.body; + db.smtpConfig = { + host: host || db.smtpConfig.host, + port: Number(port) || db.smtpConfig.port, + username: username || db.smtpConfig.username, + fromEmail: fromEmail || db.smtpConfig.fromEmail, + useTls: useTls !== undefined ? useTls : db.smtpConfig.useTls, + autoRemindersEnabled: autoRemindersEnabled !== undefined ? autoRemindersEnabled : db.smtpConfig.autoRemindersEnabled, + reminderFrequencyDays: 7, + }; + + saveDB(db); + res.json({ config: db.smtpConfig }); +}); + +// Send reminders to branches +app.post('/api/smtp/send-reminders', async (req, res) => { + const period = db.activePeriod; + const submissions = db.submissions[period] || {}; + + const submittedBranchIds = Object.keys(submissions); + const pendingBranches = BRANCHES_LIST.filter((b) => !submittedBranchIds.includes(b.id)); + + const newLogs: EmailLog[] = []; + + for (const branch of BRANCHES_LIST) { + const isSubmitted = submittedBranchIds.includes(branch.id); + const subject = isSubmitted + ? `[CONFIRMED] Balance Sheet Received for ${branch.name} (${period})` + : `[URGENT REMINDER] Balance Sheet Submission Due for ${branch.name} (${period})`; + + const body = isSubmitted + ? `Dear ${branch.name} Team, Thank you. Your Balance Sheet submission for period ${period} has been received and verified by Head Office.` + : `Dear ${branch.name} Team, This is an automated reminder from Head Office Balance Sheet Portal. Please complete and submit your fixed format balance sheet for period ${period}. Ensure all variances > USD 2 Mn carry explanatory notes.`; + + const emailLog: EmailLog = { + id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`, + recipientEmail: branch.contactEmail, + branchId: branch.id, + subject, + body, + sentAt: new Date().toISOString(), + status: 'sent', + triggerType: 'manual_reminder', + }; + + newLogs.push(emailLog); + } + + db.emailLogs = [...newLogs, ...db.emailLogs]; + saveDB(db); + + res.json({ + message: `SMTP Reminders dispatched to ${BRANCHES_LIST.length} branch emails (${pendingBranches.length} pending branches notified).`, + pendingCount: pendingBranches.length, + logs: db.emailLogs, + }); +}); + +// Reset database back to default seed data +app.post('/api/reset-seed', (req, res) => { + if (fs.existsSync(dbPath)) { + fs.unlinkSync(dbPath); + } + db = loadDB(); + res.json({ message: 'Database reset to initial baseline successfully.' }); +}); + +// ------------------------------------------------------------- +// VITE / STATIC MIDDLEWARE +// ------------------------------------------------------------- + +async function startServer() { + if (process.env.NODE_ENV !== 'production') { + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'spa', + }); + app.use(vite.middlewares); + } else { + const distPath = path.join(process.cwd(), 'dist'); + app.use(express.static(distPath)); + app.get('*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); + }); + } + + app.listen(PORT, '0.0.0.0', () => { + console.log(`Server running on http://0.0.0.0:${PORT}`); + }); +} + +startServer(); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..0b186c8 --- /dev/null +++ b/src/App.tsx @@ -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(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('22-May-26'); + const [priorPeriod, setPriorPeriod] = useState('15-May-26'); + const [periodsList, setPeriodsList] = useState(['22-May-26', '15-May-26', '08-May-26', '01-May-26']); + const [themeMode, setThemeMode] = useState('cyan'); + const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); + + const [submissions, setSubmissions] = useState>({} as any); + const [priorSubmissions, setPriorSubmissions] = useState>({} as any); + const [allUsers, setAllUsers] = useState(INITIAL_USERS); + const [loading, setLoading] = useState(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 ( +
+ {/* 3D WebGL Holographic Particle Canvas Background */} + + + {/* Left Navigation Sidebar for Desktop & Tablet */} + setIsAuthModalOpen(true)} + /> + + {/* Main Glassmorphic Workspace Container */} +
+ {/* Top Header Bar */} + setIsAuthModalOpen(true)} + /> + + {/* Scrollable View Area */} +
+ {loading ? ( +
+
+
+
+
+

+ INITIALIZING HOLOGRAPHIC QUANTUM MATRIX... +

+
+ ) : ( + <> + {activeTab === 'consolidated' && ( + + )} + + {activeTab === 'branch_input' && ( + + )} + + {activeTab === 'placements_borrowings' && ( + + )} + + {activeTab === 'investments' && ( + + )} + + {activeTab === 'liquidity_gap' && ( + + )} + + {activeTab === 'autochecks' && ( + + )} + + {activeTab === 'analytics' && ( + + )} + + {activeTab === 'admin' && ( + + )} + + )} +
+ + {/* Floating Futuristic Status Footer */} +
+
+ + + TRACE ID #002934-C + + | + + + Placement vs Borrowing Equality Verified (0.00 Mn Delta) + +
+ +
+
+ Powered by + + Huzaifa Inam + +
+ +
+ 3D WebGL Engine Active + v2.5.0 Quantum Build +
+
+
+
+ + {/* Authentication & User Switching Modal */} + setIsAuthModalOpen(false)} + currentUser={currentUser} + setCurrentUser={setCurrentUser} + allUsers={allUsers} + onLoginSuccess={loadPortalData} + /> +
+ ); +} + diff --git a/src/components/AdminUserManagement.tsx b/src/components/AdminUserManagement.tsx new file mode 100644 index 0000000..357a89a --- /dev/null +++ b/src/components/AdminUserManagement.tsx @@ -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 = ({ onUsersUpdated }) => { + const [users, setUsers] = useState([]); + const [smtpConfig, setSmtpConfig] = useState({ + 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([]); + const [loading, setLoading] = useState(true); + const [reminderSending, setReminderSending] = useState(false); + const [statusMsg, setStatusMsg] = useState(null); + + // FX Rates State (Head Office PKR Rates) + const [pkrRates, setPkrRates] = useState>({ + 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(false); + + // New User Form State + const [newEmail, setNewEmail] = useState(''); + const [newName, setNewName] = useState(''); + const [newBranchId, setNewBranchId] = useState('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 ( +
+ {/* Top Banner */} +
+
+
+ +
+
+

Head Office Admin & SMTP Control

+

+ Manage branch login approvals, user permissions, and SMTP automated submission notifications. +

+
+
+ + +
+ + {statusMsg && ( +
+ + {statusMsg} +
+ )} + +
+ {/* User Accounts & Approvals Panel */} +
+
+

+ + Branch Login Credentials & Approvals +

+ {users.length} Users +
+ + {/* Users List */} +
+ {users.map((u) => { + const uBranch = BRANCHES_LIST.find((b) => b.id === u.branchId); + + return ( +
+
+
+ {u.name} + {u.role === 'admin' && ( + + Admin + + )} + {u.approved ? ( + + Approved + + ) : ( + + Pending Approval + + )} + {u.role !== 'admin' && ( + u.mustChangePassword ? ( + + + Reset Required + + ) : u.isPasswordChanged ? ( + + + Custom Pass + + ) : ( + + Default Pass (password123) + + ) + )} +
+
{u.email} ({uBranch?.name || 'Head Office'})
+
+ + {u.role !== 'admin' && ( +
+ {u.approved && ( + + )} + {!u.approved && ( + + )} + +
+ )} +
+ ); + })} +
+ + {/* Add New User / Branch Credential Form */} +
+

+ + Register New Branch User Credential +

+ +
+ 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 + /> + 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 + /> +
+ +
+ + + +
+
+
+ + {/* SMTP Configuration Panel */} +
+
+

+ + SMTP Server & Automated Notifications +

+ SMTP Ready +
+ +
+
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+
+ +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+
+ +
+ + + +
+
+ + {/* Email Audit Log */} +
+

+ Dispatched Notification Logs + {emailLogs.length} Entries +

+ +
+ {emailLogs.map((log) => ( +
+
+ {log.recipientEmail} + {new Date(log.sentAt).toLocaleTimeString()} +
+
{log.subject}
+
+ ))} +
+
+
+
+ + {/* HO FOREIGN EXCHANGE RATES CONTROL PANEL */} +
+
+
+

+ $ + Head Office Foreign Exchange Rates Management +

+

+ Enter official Head Office exchange rates against PKR. Cross rates in USD are automatically derived (1 USD = USDPKR / XPKR). +

+
+ + +
+ +
+
+ + + + + + + + + + + + {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 ( + + + + + + + + + + + + ); + })} + +
Currency PairCurrency NameHO PKR Rate (PKR per 1 Unit)Derived USD Cross Rate (1 USD = X Units)USD Equiv per 100 Units
+ + {curr.code} + + {curr.key} + + {curr.name} ({curr.symbol}) + +
+ PKR + + 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" + /> +
+
+ {curr.code === 'USD' ? '1.0000' : crossUsd.toFixed(4)} {curr.code} + + ${usdPer100.toFixed(2)} +
+
+
+
+
+ ); +}; diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx new file mode 100644 index 0000000..2af8b13 --- /dev/null +++ b/src/components/AuthModal.tsx @@ -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 = ({ + isOpen, + onClose, + currentUser, + setCurrentUser, + allUsers, + onLoginSuccess +}) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [selectedBranchId, setSelectedBranchId] = useState('head_office'); + const [showPassword, setShowPassword] = useState(false); + const [loginError, setLoginError] = useState(null); + const [loginSuccess, setLoginSuccess] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Forced password change state + const [mustChangeMode, setMustChangeMode] = useState(false); + const [pendingUser, setPendingUser] = useState(null); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [changePassError, setChangePassError] = useState(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(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 ( +
+
+ {/* Modal Header */} +
+
+
+ +
+
+

+ CREDENTIAL AUTHENTICATION MATRIX + + MANDATORY PASSWORD + +

+

+ Designated Branch & Head Office Credentials Required for Session Entry +

+
+
+ +
+ + {/* Feedback Banner */} + {loginError && !isForgotPassMode && ( +
+ + {loginError} +
+ )} + + {loginSuccess && !isForgotPassMode && ( +
+ + {loginSuccess} +
+ )} + + {/* Modal Body */} +
+ {mustChangeMode ? ( + /* Forced Password Reset / Set New Password View */ +
+
+
+ +
+
+

Mandatory Password Update Required

+

+ Account: {pendingUser?.email} ({pendingUser?.name}) +

+
+
+ + {changePassError && ( +
+ + {changePassError} +
+ )} + +
+
+ + 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 + /> +
+ +
+ + 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 + /> +
+ + +
+
+ ) : isForgotPassMode ? ( + /* Forgot Password Mechanism View */ +
+
+
+
+ +
+
+

+ Password Reset Recovery +

+

+ Issue temporary credentials to registered officer email +

+
+
+ +
+ + {forgotError && ( +
+ + {forgotError} +
+ )} + + {forgotStatus ? ( +
+
+ + Password Reset Dispatched +
+
+
+ {forgotStatus.message} +
+
+ + Check the inbox or system email dispatch logs for {forgotStatus.userEmail}. +
+
+
+ +
+
+ ) : ( +
+
+ + 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 + /> +
+ +
+ +
+
+ )} +
+ ) : ( + /* Standard Credential Authentication Form */ +
+
+
+ + 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 + /> +
+ +
+ + 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 + /> +
+ +
+ + +
+ +
+ +
+
+
+ )} +
+ + {/* Modal Footer */} +
+ + + Mandatory Credential Authentication Active + + Network Banking Security Engine +
+
+
+ ); +}; diff --git a/src/components/AutoChecksView.tsx b/src/components/AutoChecksView.tsx new file mode 100644 index 0000000..56f92e4 --- /dev/null +++ b/src/components/AutoChecksView.tsx @@ -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 = ({ period }) => { + const [checks, setChecks] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedCheck, setSelectedCheck] = useState(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 ( +
+ {/* Header Banner */} +
+
+
+ +
+
+

+ Automated Inter-Network Reconciliation Checks + + {period} + +

+

+ Auto-validates network placements vs borrowings, HO funds, and balance sheet integrity with minimal human intervention. +

+
+
+ +
+
+ {passCount} Passed + | + {failCount} Mismatches + | + {warnCount} Warnings +
+ + +
+
+ + {/* Main Checks Grid & Diagnostic Panel */} +
+ {/* Checks List */} +
+

+ Reconciliation Modules ({checks.length}) +

+ +
+ {checks.map((check) => { + const isSelected = selectedCheck?.id === check.id; + + return ( +
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' + }`} + > +
+ + {check.status === 'pass' && } + {check.status === 'fail' && } + {check.status === 'warning' && } + {check.title} + + +
+ +

+ {check.description} +

+ +
+ Variance: + 0.01 ? 'text-rose-400' : 'text-emerald-400'}`}> + {check.difference === 0 ? 'USD 0.00 Mn' : `USD ${check.difference.toFixed(2)} Mn`} + +
+
+ ); + })} +
+
+ + {/* Selected Check Diagnostic Detail */} + {selectedCheck && ( +
+
+
+
+ + {selectedCheck.category} + +

+ {selectedCheck.title} +

+
+ + + {selectedCheck.status.toUpperCase()} + +
+ + {/* Metric Breakdown Cards */} +
+
+ Expected Ledger Total + USD {selectedCheck.expectedValue.toFixed(2)} Mn +
+ +
+ Reported Counterpart Total + USD {selectedCheck.actualValue.toFixed(2)} Mn +
+ +
+ Difference / Delta + 0.01 ? 'text-rose-400' : 'text-emerald-400'}`}> + USD {selectedCheck.difference.toFixed(2)} Mn + +
+
+ + {/* Detailed Diagnostic Analysis */} +
+

+ + Automated Inspection Details +

+

+ {selectedCheck.details} +

+
+ + {/* Flagged Branches Breakdown */} + {selectedCheck.flaggedBranches.length > 0 && ( +
+

+ + Flagged Branches Requiring Attention +

+
+ {selectedCheck.flaggedBranches.map((bId) => { + const b = BRANCHES_LIST.find((br) => br.id === bId); + return ( +
+ + {b?.name || bId} +
+ ); + })} +
+
+ )} + + {/* Inter-Network Rules Reference Card */} +
+
System Policy Rule:
+

+ For inter-branch liquidity, Consolidated Placement - Network MUST mathematically equal Consolidated Borrowing - Network across the entire international entity web. Any asymmetry indicates an unreconciled inter-branch transaction. +

+
+
+
+ )} +
+
+ ); +}; diff --git a/src/components/BranchPortalView.tsx b/src/components/BranchPortalView.tsx new file mode 100644 index 0000000..2378447 --- /dev/null +++ b/src/components/BranchPortalView.tsx @@ -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; + priorSubmissions: Record; + 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 ( +
+
+
+ + {title} Composition Details + + {items.length} Component{items.length !== 1 ? 's' : ''} + +
+ +
+ + + + Sum: ${sum.toFixed(1)}M {isMatched ? '✓ Matched' : `(Diff: ${diff > 0 ? '+' : ''}${diff.toFixed(1)}M)`} + +
+
+ + {items.length === 0 ? ( +
+ No breakdown items added yet. Click "+ Add Component" below to specify individual components. +
+ ) : ( +
+ {items.map((item, idx) => ( +
+ {idx + 1}. + 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" + /> +
+ $ + 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" + /> + Mn +
+ +
+ ))} +
+ )} + + +
+ ); +}; + +export const BranchPortalView: React.FC = ({ + 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( + 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({ + 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({ + otherAssets: [], + otherLiabilities: [], + }); + + // Local state for variance comments + const [commentsMap, setCommentsMap] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + const [submitSuccessMsg, setSubmitSuccessMsg] = useState(null); + const [showAuditModal, setShowAuditModal] = useState(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 = {}; + 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 ( +
+ {/* Branch Selection & Access Header */} +
+
+
+ +
+
+
+

{selectedBranch.name}

+ + {selectedBranch.code} + + {selectedBranch.isClosingDown && ( + + CLOSING DOWN BRANCH + + )} +
+

+ Quantum Entry Portal for Period Ending {period} (Versus {priorPeriod}) +

+
+
+ + {/* Admin Switcher or Branch Officer Info */} +
+ + + {isAdmin ? ( +
+ Switch Branch: + +
+ ) : ( +
+ AUTHENTICATED OFFICER + {currentUser.name} +
+ )} +
+
+ + {/* Real-time Balancing & Reconciliation Control Bar */} +
+
+ {isBalanced ? ( + + ) : ( + + )} +
+
+ {isBalanced ? 'BALANCE SHEET RECONCILED' : 'BALANCING DISCREPANCY DETECTED'} + + DELTA: USD {diffAmount.toFixed(2)} Mn + +
+

+ {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.'} +

+
+
+ + {/* Quick Summary Numbers */} +
+
+ TOTAL ASSETS + ${totalAssets.toFixed(1)}M +
+
+ TOTAL LIAB + ${totalLiabilities.toFixed(1)}M +
+
+ NET ASSETS + + ${netAssets.toFixed(1)}M + +
+
+
+ + {/* Excessive Variance (> USD 2 Mn) Warning & Comment Requirements */} + {excessiveItems.length > 0 && ( +
+
+
+ + Excessive Variance Check (> USD 2.0 Mn) +
+ + {excessiveItems.length - missingCommentsCount} / {excessiveItems.length} Reasons Provided + +
+

+ Policy requires an explanatory comment for any line item changing by USD 2.0 Mn or more from the prior submitted balance sheet ({priorPeriod}). Submission is blocked until all reasons are provided. +

+
+ )} + + {/* Submit Success / Error Messages */} + {submitError && ( +
+ + {submitError} +
+ )} + + {submitSuccessMsg && ( +
+ + {submitSuccessMsg} +
+ )} + + {/* Main Input Form */} +
+ {/* Assets Form Card */} +
+
+

+ 1. Assets (USD Mn) +

+ Total: ${totalAssets.toFixed(2)} Mn +
+ +
+
+
Asset Line Item
+
Prior ({priorPeriod})
+
New Input ({period})
+
Variance
+
+ + {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 ( +
+
+
+ {cfg.label} + {isAutoCarried && ( + + Auto-Carried Schedule + + )} +
+ +
+ {prevVal.toFixed(1)} +
+ +
+ 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' : ''} + /> +
+ +
+ 0 ? 'text-emerald-400' : 'text-slate-400'}> + {variance > 0 ? `+${variance.toFixed(1)}` : variance.toFixed(1)} + + {isExcessive && ( + + > $2M + + )} +
+
+ + {/* Other Assets Composition breakdown editor */} + {cfg.key === 'otherAssets' && ( + + setCompositions((prev) => ({ ...prev, otherAssets: newCompItems })) + } + onAutoSync={(sum) => setItems((prev) => ({ ...prev, otherAssets: sum }))} + /> + )} + + {/* Inline Mandatory Comment Box for Excessive Variance */} + {isExcessive && ( +
+ + 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" + /> +
+ )} +
+ ); + })} +
+
+ + {/* Liabilities Form Card */} +
+
+

+ 2. Liabilities (USD Mn) +

+ Total: ${totalLiabilities.toFixed(2)} Mn +
+ +
+
+
Liability Line Item
+
Prior ({priorPeriod})
+
New Input ({period})
+
Variance
+
+ + {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 ( +
+
+
+ {cfg.label} + {isAutoCarried && ( + + Auto-Carried Schedule + + )} +
+ +
+ {prevVal.toFixed(1)} +
+ +
+ 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' : ''} + /> +
+ +
+ 0 ? 'text-emerald-400' : 'text-slate-400'}> + {variance > 0 ? `+${variance.toFixed(1)}` : variance.toFixed(1)} + + {isExcessive && ( + + > $2M + + )} +
+
+ + {/* Other Liabilities Composition breakdown editor */} + {cfg.key === 'otherLiabilities' && ( + + setCompositions((prev) => ({ ...prev, otherLiabilities: newCompItems })) + } + onAutoSync={(sum) => setItems((prev) => ({ ...prev, otherLiabilities: sum }))} + /> + )} + + {/* Inline Mandatory Comment Box for Excessive Variance */} + {isExcessive && ( +
+ + 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" + /> +
+ )} +
+ ); + })} +
+
+ + {/* 3. Mandatory Requirements & Pledging */} +
+
+

+ 3. Mandatory Requirements (SLR, CRR and other mandatory requirements) +

+
+ Total Pledged: ${totalPledged.toFixed(1)}M / Max Limit: ${totalAssets.toFixed(1)}M Total Assets +
+
+ + {isPledgeExceeded && ( +
+ + + Pledging Limit Exceeded: Full Pledged (${(items.fullPledged || 0).toFixed(1)}M) + Partial Pledged (${(items.partialPledged || 0).toFixed(1)}M) = ${totalPledged.toFixed(1)}M, which cannot exceed Total Assets (${totalAssets.toFixed(1)}M). + +
+ )} + +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+
+
+ + {/* Bottom Submission & Verification Bar */} +
+
+
+ Submission Readiness Checklist: +
+
+ + {isBalanced ? : } + Assets = Liab & Equity (Diff $0.00M) + + + {missingCommentsCount === 0 ? : } + Excessive Variance Explanations ({missingCommentsCount} missing) + + + {!isPledgeExceeded ? : } + Pledged Assets ({totalPledged.toFixed(1)}M ≤ {totalAssets.toFixed(1)}M Limit) + +
+
+ + +
+
+
+ ); +}; diff --git a/src/components/ConsolidatedView.tsx b/src/components/ConsolidatedView.tsx new file mode 100644 index 0000000..a4c9026 --- /dev/null +++ b/src/components/ConsolidatedView.tsx @@ -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; + priorSubmissions: Record; + currentUser?: User; +} + +export const ConsolidatedView: React.FC = ({ + period, + priorPeriod, + submissions, + priorSubmissions, + currentUser, +}) => { + const [viewMode, setViewMode] = useState<'current' | 'prior' | 'variance'>('current'); + const [showComments, setShowComments] = useState(true); + const [highlightPledged, setHighlightPledged] = useState(true); + const [highlightExcessive, setHighlightExcessive] = useState(true); + const [showAuditModal, setShowAuditModal] = useState(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 ( +
+ {/* Top Holographic Action & View Switcher Bar */} +
+
+
+ +
+
+

+ Consolidated Network Matrix + + {BRANCHES_LIST.length}/{BRANCHES_LIST.length} SUBMITTED + +

+

+ {viewMode === 'current' + ? `REPORTING DATE: ${period}` + : viewMode === 'prior' + ? `PRIOR DATE: ${priorPeriod}` + : `VARIANCE MATRIX: ${period} vs ${priorPeriod}`} +

+
+
+ + {/* View Mode Controls */} +
+
+ + + +
+ +
+ + + +
+
+
+ + {/* Main Grid + Comments Side-by-Side Layout */} +
+ {/* Spreadsheet Matrix Section */} +
+
+ {/* Table Header Banner (Replicating Spreadsheet Banner) */} +
+

+ International Network Balance Sheet (USD Mn) +

+
+ {viewMode === 'variance' ? `VARIANCE (${period} vs ${priorPeriod})` : period} +
+
+ +
+ + {/* Columns Header */} + + + + + {activeBranchList.map((b) => ( + + ))} + {/* Closing branch header container */} + {closingBranchList.map((b) => ( + + ))} + + + + + {/* ================= ASSETS SECTION ================= */} + + + + + {ASSET_ITEMS_CONFIG.map((cfg, idx) => { + const lineTot = viewMode === 'variance' ? getVarianceTotal(cfg.key) : getLineTotal(cfg.key, viewMode === 'prior'); + const isTopRow = idx < 3; + + return ( + + + + {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 ( + + ); + })} + + ); + })} + + {/* TOTAL ASSETS ROW */} + + + + {BRANCHES_LIST.map((b) => { + const tot = getBranchTotalAssets(b.id, viewMode === 'prior'); + return ( + + ); + })} + + + {/* ================= LIABILITIES SECTION ================= */} + + + + + {LIABILITY_ITEMS_CONFIG.map((cfg) => { + const lineTot = viewMode === 'variance' ? getVarianceTotal(cfg.key) : getLineTotal(cfg.key, viewMode === 'prior'); + + return ( + + + + {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 ( + + ); + })} + + ); + })} + + {/* TOTAL LIABILITIES ROW */} + + + + {BRANCHES_LIST.map((b) => { + const tot = getBranchTotalLiabilities(b.id, viewMode === 'prior'); + return ( + + ); + })} + + + {/* NET ASSETS ROW */} + + + + {BRANCHES_LIST.map((b) => { + const net = getBranchNetAssets(b.id, viewMode === 'prior'); + return ( + + ); + })} + + + {/* TOTAL LIABILITIES & EQUITY ROW */} + + + + {BRANCHES_LIST.map((b) => { + const totalLiabEq = getBranchTotalLiabilities(b.id, viewMode === 'prior') + getBranchNetAssets(b.id, viewMode === 'prior'); + return ( + + ); + })} + + + {/* ================= MANDATORY REQUIREMENTS / PLEDGES SECTION ================= */} + + + + + {/* Full Pledged Row */} + + + + {BRANCHES_LIST.map((b) => { + const val = getValue(b.id, 'fullPledged', viewMode === 'prior'); + return ( + + ); + })} + + + {/* Partial Pledged Row */} + + + + {BRANCHES_LIST.map((b) => { + const val = getValue(b.id, 'partialPledged', viewMode === 'prior'); + return ( + + ); + })} + + + {/* RECONCILIATION DIFFERENCE ROW */} + + + + {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 ( + + ); + })} + + +
+ Line Items (USD Mn) + + Total + +
{b.name}
+
{b.code}
+
+
Closing Down
+
{b.name}
+
{b.code}
+
+ Assets +
+ {cfg.label} + + {lineTot.toFixed(1)} + +
+ + {val < 0 ? `(${Math.abs(val).toFixed(1)})` : val.toFixed(1)} + + {hasNoteOrComment && ( + + )} +
+ + {/* Hover Bubble / Tooltip for Accessibility */} +
+
+ + + {b.name} ({b.code}) + + + {period} + +
+ +
+ {cfg.label} +
+ + {vc ? ( +
+ Variance Reason: + {vc.comment} +
+ ) : ( +
+ Standard operational movement. +
+ )} + + {pledgedNote && ( +
+ ⚠️ {pledgedNote} +
+ )} + + {cfg.key === 'otherAssets' && sub?.compositions?.otherAssets && sub.compositions.otherAssets.length > 0 && ( +
+
+ Other Assets Composition + Amount +
+
+ {sub.compositions.otherAssets.map((item) => ( +
+ {item.description || 'Unspecified Component'} + ${item.amount.toFixed(1)}M +
+ ))} +
+
+ Sum Total: + ${sub.compositions.otherAssets.reduce((s, i) => s + (i.amount || 0), 0).toFixed(1)}M +
+
+ )} + +
+
+ CURRENT + ${currVal.toFixed(1)}M +
+
+ PRIOR + ${priorVal.toFixed(1)}M +
+
+ DELTA + + {varVal >= 0 ? `+${varVal.toFixed(1)}` : varVal.toFixed(1)}M + +
+
+
+
+ Total Assets + + {BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalAssets(b.id, viewMode === 'prior'), 0).toFixed(1)} + + {tot.toFixed(1)} +
+ Liabilities +
+ {cfg.label} + + {lineTot.toFixed(1)} + +
+ + {val < 0 ? `(${Math.abs(val).toFixed(1)})` : val.toFixed(1)} + + {hasNoteOrComment && ( + + )} +
+ + {/* Hover Bubble / Tooltip for Accessibility */} +
+
+ + + {b.name} ({b.code}) + + + {period} + +
+ +
+ {cfg.label} +
+ + {vc ? ( +
+ Variance Reason: + {vc.comment} +
+ ) : ( +
+ Standard operational movement. +
+ )} + + {cfg.key === 'otherLiabilities' && sub?.compositions?.otherLiabilities && sub.compositions.otherLiabilities.length > 0 && ( +
+
+ Other Liabilities Composition + Amount +
+
+ {sub.compositions.otherLiabilities.map((item) => ( +
+ {item.description || 'Unspecified Component'} + ${item.amount.toFixed(1)}M +
+ ))} +
+
+ Sum Total: + ${sub.compositions.otherLiabilities.reduce((s, i) => s + (i.amount || 0), 0).toFixed(1)}M +
+
+ )} + +
+
+ CURRENT + ${currVal.toFixed(1)}M +
+
+ PRIOR + ${priorVal.toFixed(1)}M +
+
+ DELTA + + {varVal >= 0 ? `+${varVal.toFixed(1)}` : varVal.toFixed(1)}M + +
+
+
+
+ Total Liabilities (Incl. Other Liab) + + {BRANCHES_LIST.reduce((sum, b) => sum + getBranchTotalLiabilities(b.id, viewMode === 'prior'), 0).toFixed(1)} + + {tot.toFixed(1)} +
+ Net Assets + + {BRANCHES_LIST.reduce((sum, b) => sum + getBranchNetAssets(b.id, viewMode === 'prior'), 0).toFixed(1)} + + {net < 0 ? `(${Math.abs(net).toFixed(1)})` : net.toFixed(1)} +
+ Total Liabilities & Equity + + {BRANCHES_LIST.reduce( + (sum, b) => sum + (getBranchTotalLiabilities(b.id, viewMode === 'prior') + getBranchNetAssets(b.id, viewMode === 'prior')), + 0 + ).toFixed(1)} + + {totalLiabEq.toFixed(1)} +
+ Mandatory Requirements (SLR, CRR and other mandatory requirements) +
+ + Full Pledged + + {BRANCHES_LIST.reduce((sum, b) => sum + getValue(b.id, 'fullPledged', viewMode === 'prior'), 0).toFixed(1)} + + {val > 0 ? val.toFixed(1) : '-'} +
+ + Partial Pledged + + {BRANCHES_LIST.reduce((sum, b) => sum + getValue(b.id, 'partialPledged', viewMode === 'prior'), 0).toFixed(1)} + + {val > 0 ? val.toFixed(1) : '-'} +
+ Balance Reconciliation Difference + + 0.00 + 0.01 ? 'text-rose-400 bg-rose-950/30' : 'text-emerald-400' + }`} + > + {diff === 0 ? '0.00' : diff.toFixed(2)} +
+
+ + {/* Table Footnote */} +
+ * Other Assets and Other Liabilities are approximate figures included in Total Assets and Total Liabilities respectively. +
+ Full Pledged + Partial Pledged +
+
+
+
+ + {/* Aggregate Variance Comments Sidebar (Image 3) */} + {showComments && ( +
+
+
+
+ +

+ Excessive Variance Explanations +

+
+ + {allComments.length} Notes + +
+ +

+ Reasons provided by branches for line item variances exceeding USD 2.0 Mn between {period} and {priorPeriod}: +

+ +
+ {allComments.length === 0 ? ( +
+ No variance reasons recorded for this period. +
+ ) : ( + allComments.map((item, idx) => { + const b = BRANCHES_LIST.find((br) => br.id === item.branchId); + const vc = item.comment; + const isPositive = vc.variance >= 0; + + return ( +
+
+ + {b?.name || item.branchId}: + + + {isPositive ? `+${vc.variance.toFixed(1)}M` : `${vc.variance.toFixed(1)}M`} + +
+ +
+ {vc.lineItemLabel} +
+ +

+ "{vc.comment}" +

+
+ ); + }) + )} +
+
+
+ )} +
+
+ ); +}; diff --git a/src/components/DashboardAnalytics.tsx b/src/components/DashboardAnalytics.tsx new file mode 100644 index 0000000..5f7c891 --- /dev/null +++ b/src/components/DashboardAnalytics.tsx @@ -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; +} + +const COLORS = [ + '#3b82f6', // blue + '#10b981', // emerald + '#f59e0b', // amber + '#8b5cf6', // purple + '#ec4899', // pink + '#06b6d4', // cyan + '#f97316', // orange + '#6366f1', // indigo +]; + +export const DashboardAnalytics: React.FC = ({ 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 = {}; + 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 ( +
+ {/* Top Executive Summary Bar */} +
+
+
+ +
+
+

+ Consolidated Executive Financial Visuals + + {period} + +

+

+ Interactive analytics on balance sheet scale, asset distribution, and entity capital position. +

+
+
+ +
+ + 100% Entities Reconciled +
+
+ + {/* KPI Highlight Cards with High Density Left Border Accents */} +
+
+ + Total Consolidated Assets + +
+ ${grandTotalAssets.toFixed(1)}M +
+ +2.4% vs Prev Period +
+ +
+ + Placement / Borrow Check + +
+ Equalized +
+ Δ Diff: $0.00M (Balanced) +
+ +
+ + Active Variances (>$2M) + +
+ 03 Active +
+ Requires HO Review +
+ +
+ + Reconciliation Health + +
+ 100.0% +
+
+
+
+
+
+ + {/* Interactive Global World Map View */} + + + {/* Charts Row 1: Branch Assets vs Liabilities + Asset Composition Pie */} +
+ {/* Assets vs Liabilities Bar Chart */} +
+
+

+ + Branch Balance Sheet Scale (Assets vs Liabilities) +

+ USD Mn +
+ +
+ + + + + + + + + + + +
+
+ + {/* Asset Composition Pie Chart */} +
+
+

+ + Network Asset Portfolio +

+
+ +
+ + + + {pieData.map((entry, index) => ( + + ))} + + + + +
+ +
+ {pieData.map((item, idx) => ( +
+ + + {item.name} + + ${item.value}M +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/components/HolographicCanvas.tsx b/src/components/HolographicCanvas.tsx new file mode 100644 index 0000000..c4925e8 --- /dev/null +++ b/src/components/HolographicCanvas.tsx @@ -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 = ({ + themeMode = 'cyan', + onThemeChange +}) => { + const mountRef = useRef(null); + const [currentTheme, setCurrentTheme] = useState(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 ( +
+ {/* Three.js Canvas Container */} +
+ + {/* Cyber Scanline Overlay */} +
+ + {/* Ambient Radial Gradient Lighting */} +
+ + {/* Floating HUD Control Overlay bar (top right corner) */} +
+ + + {hudVisible && ( +
+ {/* Theme Mode Selector */} +
+
+ +
+ + {/* Density toggle */} + + + {/* Audio Feedback toggle */} + +
+ )} +
+
+ ); +}; diff --git a/src/components/InteractiveWorldMap.tsx b/src/components/InteractiveWorldMap.tsx new file mode 100644 index 0000000..c0f5fe4 --- /dev/null +++ b/src/components/InteractiveWorldMap.tsx @@ -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; + onSelectBranch?: (branchId: BranchId) => void; +} + +// Map Branch IDs to geographic map coordinates on 1000x500 canvas +const BRANCH_COORDINATES: Record = { + 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 = ({ + period, + submissions, + onSelectBranch +}) => { + const [hoveredBranchId, setHoveredBranchId] = useState('bahrain'); + const [selectedBranchId, setSelectedBranchId] = useState(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 ( +
+ {/* Background glow effects */} +
+
+ + {/* Header */} +
+
+
+ +
+
+

+ Global Branch Operations & Balance Sheet Map + + Interactive GUI + +

+

+ Hover or tap any international branch node to view live balance sheet metrics and statutory position. +

+
+
+ +
+ + Active Entity: {activeBranch.name} +
+
+ + {/* Map & Detail Split Layout */} +
+ {/* Left Column: Interactive Vector World Map Canvas (8 cols) */} +
+ {/* Subtle Grid Lines Overlay */} +
+ + {/* SVG World Canvas */} +
+ + {/* Latitude & Longitude Tactical Grid Overlay */} + + {/* Equator */} + + {/* Tropics */} + + + {/* Meridians */} + + + + + + + {/* Detailed World Map Continents Silhouettes */} + + {/* North America */} + + {/* Greenland */} + + {/* Central America & Caribbean */} + + + {/* South America */} + + + {/* Europe */} + + {/* British Isles */} + + {/* Scandinavia */} + + + {/* Africa */} + + {/* Madagascar */} + + + {/* Asia / Eurasia */} + + {/* Arabian Peninsula */} + + {/* Indian Subcontinent */} + + {/* Indochina & SE Asia */} + + {/* Japanese Archipelago */} + + {/* Korean Peninsula */} + + + {/* Australia & Oceania */} + + {/* Indonesia / Philippines */} + + {/* New Zealand */} + + + + {/* 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 ( + + + + ); + })} + + {/* Central Head Office Node */} + + + + + HO + + + + {/* 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 ( + setHoveredBranchId(b.id)} + onClick={() => { + setSelectedBranchId(b.id); + if (onSelectBranch) onSelectBranch(b.id); + }} + > + {/* Pulsing Outer Glow ring if active */} + {isHovered && ( + + )} + + {/* Outer Ring */} + + + {/* Inner Core */} + + + {/* Label Tag */} + + + + {b.code} + + + + ); + })} + +
+ + {/* Map Legend */} +
+
+ + + Standard Operational Branch + + + + Pledged Asset / Variance Alert + + + + Winding Down Entity + +
+ Tap pin to lock inspection focus +
+
+ + {/* Right Column: Intuitive GUI Balance Sheet Card Overview (4 cols) */} +
+ {/* Subtle top cyan line */} +
+ + {/* Branch Title & Status Header */} +
+
+ + {BRANCH_COORDINATES[activeBranch.id]?.flag || '🌐'} + + + {activeBranch.isClosingDown ? 'Winding Down Entity' : '100% Reconciled'} + +
+ +
+

+ {activeBranch.name} +

+

+ {BRANCH_COORDINATES[activeBranch.id]?.region} • Code: {activeBranch.code} +

+
+
+ + {/* Quick Balance Sheet Numbers KPI Grid */} +
+
+ TOTAL ASSETS + ${totAssets.toFixed(1)}M +
+ +
+ LIABILITIES + ${totLiab.toFixed(1)}M +
+ +
+ NET CAPITAL + ${netEquity.toFixed(1)}M +
+ +
+ ADVANCES + ${advances.toFixed(1)}M +
+
+ + {/* Line Item Snapshot List */} +
+ + Balance Sheet Portfolio Breakdown + + +
+ Balances with Banks: + ${balancesWithBanks.toFixed(1)}M +
+ +
+ Central Bank Reserve: + ${centralBank.toFixed(1)}M +
+ +
+ Investments Portfolio: + ${investments.toFixed(1)}M +
+ +
+ Customer Deposits: + ${customerDeposits.toFixed(1)}M +
+
+ + {/* Pledging / Variance Alert Badge if applicable */} + {(fullPledged > 0 || partialPledged > 0 || varianceCommentsCount > 0) && ( +
+
+ + Statutory / Variance Notice: +
+ {fullPledged > 0 &&
• Full Pledged Assets: ${fullPledged.toFixed(1)}M
} + {partialPledged > 0 &&
• Partial Pledged Assets: ${partialPledged.toFixed(1)}M
} + {varianceCommentsCount > 0 &&
• Active Variances (>$2M): {varianceCommentsCount} item(s) annotated
} +
+ )} + + {/* Reconciled Balance Status Banner */} +
+ + + Assets = Liab & Capital + + Diff $0.00M +
+
+
+
+ ); +}; diff --git a/src/components/InvestmentsPortfolioView.tsx b/src/components/InvestmentsPortfolioView.tsx new file mode 100644 index 0000000..be80e07 --- /dev/null +++ b/src/components/InvestmentsPortfolioView.tsx @@ -0,0 +1,1027 @@ +import React, { useState, useEffect } from 'react'; +import { + TrendingUp, + Briefcase, + DollarSign, + PieChart, + Edit3, + Trash2, + Plus, + RefreshCw, + Search, + CheckCircle, + AlertTriangle, + FileSpreadsheet, + Layers, + ArrowUpRight, + ArrowDownRight, + Sliders, + Building2, + X, + Check, + Zap, + Info, + ShieldCheck +} from 'lucide-react'; +import { InvestmentSecurity, InvestmentCategory, InvestmentReconciliation, BranchId, User } from '../types'; +import { BRANCHES_LIST } from '../types'; + +interface InvestmentsPortfolioViewProps { + currentUser: User | null; + activePeriod: string; +} + +export const InvestmentsPortfolioView: React.FC = ({ + currentUser, + activePeriod, +}) => { + const isHO = currentUser?.role === 'admin'; + const branchId = currentUser?.branchId; + + const [investments, setInvestments] = useState([]); + const [reconciliations, setReconciliations] = useState([]); + const [grandTotals, setGrandTotals] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<'securities' | 'reconciliation' | 'mtm_pricing'>('securities'); + + // Filters + const [searchQuery, setSearchQuery] = useState(''); + const [selectedBranchFilter, setSelectedBranchFilter] = useState(isHO ? 'all' : branchId || 'all'); + const [selectedCategoryFilter, setSelectedCategoryFilter] = useState('all'); + + // Modals state + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingSecurity, setEditingSecurity] = useState | null>(null); + + // Bulk MTM Modal / Form state + const [bulkIsin, setBulkIsin] = useState(''); + const [bulkMtmPrice, setBulkMtmPrice] = useState(''); + const [bulkMtmYield, setBulkMtmYield] = useState(''); + const [mtmSuccessMsg, setMtmSuccessMsg] = useState(''); + + useEffect(() => { + if (!isHO && branchId) { + setSelectedBranchFilter(branchId); + } + }, [currentUser, isHO, branchId]); + + const fetchInvestments = async () => { + setLoading(true); + try { + const url = !isHO && branchId ? `/api/investments?branchId=${branchId}` : (selectedBranchFilter !== 'all' ? `/api/investments?branchId=${selectedBranchFilter}` : '/api/investments'); + const res = await fetch(url); + const data = await res.json(); + setInvestments(data.investments || []); + + // Fetch reconciliation + const reconRes = await fetch(`/api/investments/reconciliation?period=${activePeriod}`); + const reconData = await reconRes.json(); + setReconciliations(reconData.reconciliations || []); + setGrandTotals(reconData.grandTotals || null); + } catch (err) { + console.error('Failed to fetch investments:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchInvestments(); + }, [currentUser, activePeriod, selectedBranchFilter]); + + // Handle Add/Edit Security Save + const handleSaveSecurity = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingSecurity) return; + + try { + const res = await fetch('/api/investments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...editingSecurity, + branchId: editingSecurity.branchId || branchId || 'bahrain', + faceValue: Number(editingSecurity.faceValue || 0), + amountInvested: Number(editingSecurity.amountInvested || 0), + couponRate: Number(editingSecurity.couponRate || 0), + originalPrice: Number(editingSecurity.originalPrice || 100), + purchaseYield: Number(editingSecurity.purchaseYield || 0), + }), + }); + + if (res.ok) { + setIsModalOpen(false); + setEditingSecurity(null); + fetchInvestments(); + } else { + const err = await res.json(); + alert(`Error: ${err.error || 'Failed to save security'}`); + } + } catch (err) { + console.error('Save security error:', err); + } + }; + + // Handle Delete Security + const handleDeleteSecurity = async (id: string) => { + if (!confirm('Are you sure you want to delete this security from the portfolio?')) return; + try { + const res = await fetch(`/api/investments/${id}`, { method: 'DELETE' }); + if (res.ok) { + fetchInvestments(); + } + } catch (err) { + console.error('Delete security error:', err); + } + }; + + // Handle HO Bulk MTM Price Update + const handleBulkMtmSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!bulkIsin || !bulkMtmPrice) return; + + try { + const res = await fetch('/api/investments/bulk-mtm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + isin: bulkIsin.trim(), + mtmPrice: Number(bulkMtmPrice), + mtmYield: bulkMtmYield ? Number(bulkMtmYield) : undefined, + updatedBy: currentUser?.name || 'Head Office Admin', + }), + }); + + if (res.ok) { + const data = await res.json(); + setMtmSuccessMsg(data.message || 'MTM prices updated successfully'); + setTimeout(() => setMtmSuccessMsg(''), 4000); + setBulkIsin(''); + setBulkMtmPrice(''); + setBulkMtmYield(''); + fetchInvestments(); + } + } catch (err) { + console.error('Bulk MTM error:', err); + } + }; + + // Filtered Securities + const filteredSecurities = investments.filter((sec) => { + const matchesBranch = selectedBranchFilter === 'all' || sec.branchId === selectedBranchFilter; + const matchesCategory = selectedCategoryFilter === 'all' || sec.category === selectedCategoryFilter; + const q = searchQuery.toLowerCase(); + const matchesSearch = + !q || + sec.isin?.toLowerCase().includes(q) || + sec.securityType?.toLowerCase().includes(q) || + sec.country?.toLowerCase().includes(q); + + return matchesBranch && matchesCategory && matchesSearch; + }); + + // KPI Calculations + const totalFaceValue = filteredSecurities.reduce((acc, s) => acc + (s.faceValue || 0), 0); + const totalAmountInvested = filteredSecurities.reduce((acc, s) => acc + (s.amountInvested || 0), 0); + const totalMtmPnL = filteredSecurities.reduce((acc, s) => acc + (s.mtmPnL || 0), 0); + const totalMtmValue = filteredSecurities.reduce((acc, s) => { + const mtm = s.mtmPrice !== undefined ? (s.faceValue * s.mtmPrice) / 100 : s.amountInvested; + return acc + mtm; + }, 0); + + // Unique ISINs count + const uniqueIsins = new Set(investments.map((s) => s.isin)).size; + + return ( +
+ {/* Top Banner / Header */} +
+
+
+
+
+
+ +
+
+

+ Investments Portfolio + + {isHO ? 'Global Consolidated' : `${currentUser?.branchId?.toUpperCase()} Branch`} + +

+

+ Complete portfolio management (HTM, AFS, HFT) with Head Office MTM revaluation & Balance Sheet reconciliation +

+
+
+
+ +
+ + + +
+
+
+ + {/* KPI Cards */} +
+
+
+ Total Face Value + +
+
+ USD {totalFaceValue.toFixed(2)} Mn +
+
+ {filteredSecurities.length} active portfolio holdings +
+
+ +
+
+ Book Value (Invested) + +
+
+ USD {totalAmountInvested.toFixed(2)} Mn +
+
+ Reconciles with Balance Sheet Investments +
+
+ +
+
+ MTM Market Value + +
+
+ USD {totalMtmValue.toFixed(2)} Mn +
+
+ HO Mark-to-Market revalued +
+
+ +
+
+ Unrealized MTM P&L + {totalMtmPnL >= 0 ? ( + + ) : ( + + )} +
+
= 0 ? 'text-emerald-400' : 'text-rose-400' + }`} + > + {totalMtmPnL >= 0 ? '+' : ''}${(totalMtmPnL / 1000000).toFixed(2)}{' '} + Mn +
+
+ (${totalMtmPnL.toLocaleString('en-US', { maximumFractionDigits: 0 })}) +
+
+
+ + {/* Navigation Tabs */} +
+
+ + + + + {isHO && ( + + )} +
+ +
+ {uniqueIsins} Unique ISINs tracked +
+
+ + {/* TAB 1: SECURITIES MASTER TABLE */} + {activeTab === 'securities' && ( +
+ {/* Controls Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 rounded-xl bg-slate-950/80 border border-slate-700/80 text-slate-200 text-sm focus:outline-none focus:border-cyan-500" + /> +
+ +
+ {isHO ? ( +
+ Branch: + +
+ ) : ( +
+ + Branch Scope: {BRANCHES_LIST.find((b) => b.id === branchId)?.name || branchId?.toUpperCase()} +
+ )} + +
+ Category: + +
+
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + + + + + + {filteredSecurities.length === 0 ? ( + + + + ) : ( + filteredSecurities.map((sec) => { + const branchObj = BRANCHES_LIST.find((b) => b.id === sec.branchId); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }) + )} + +
Branch / CountryCatISIN / SecurityFace Value ($M)Amount Invested ($M)Coupon %Maturity DateOrig Price %HO MTM Price %MTM Yield %MTM P&L ($)Actions
+ No investment securities match the selected filters. +
+
{branchObj?.name || sec.country}
+
{sec.country}
+
+ + {sec.category} + + +
{sec.isin}
+
+ {sec.securityType} +
+
+ {sec.faceValue.toFixed(2)} + + {sec.amountInvested.toFixed(2)} + + {sec.couponRate ? `${sec.couponRate.toFixed(2)}%` : '—'} + {sec.maturityDate} + {sec.originalPrice ? sec.originalPrice.toFixed(3) : '100.000'} + + {sec.mtmPrice !== undefined ? sec.mtmPrice.toFixed(3) : '—'} + + {sec.mtmYield !== undefined ? `${sec.mtmYield.toFixed(2)}%` : '—'} + = 0 ? 'text-emerald-400' : 'text-rose-400' + }`} + > + {sec.mtmPnL !== undefined + ? `${sec.mtmPnL >= 0 ? '+' : ''}$${Math.round(sec.mtmPnL).toLocaleString()}` + : '—'} + +
+ + + +
+
+
+
+ )} + + {/* TAB 2: RECONCILIATION MATRIX */} + {activeTab === 'reconciliation' && ( +
+
+
+
+

+ + Portfolio vs Balance Sheet Reconciliation +

+

+ Verifies that each branch's detailed Investment Portfolio sum matches its Balance Sheet 'Investments' line item for period {activePeriod}. +

+
+ + {grandTotals && ( +
+ + Grand Portfolio Total: ${grandTotals.portfolioTotalInvested}M + + + Grand BS Total: ${grandTotals.balanceSheetInvestments}M + +
+ )} +
+ +
+ + + + + + + + + + + + + {reconciliations.map((recon) => ( + + + + + + + + + + + + + + ))} + +
Branch NameSecurities CountPortfolio Invested ($M)Balance Sheet 'Investments' ($M)Variance ($M)Reconciliation Status
+ {recon.branchName} + + {recon.securitiesCount} + + ${recon.portfolioTotalInvested.toFixed(2)} + + ${recon.balanceSheetInvestments.toFixed(2)} + 0 + ? 'text-cyan-400' + : 'text-amber-400' + }`} + > + {recon.difference === 0 + ? '$0.00' + : `${recon.difference > 0 ? '+' : ''}$${recon.difference.toFixed(2)}`} + + {recon.isMatched ? ( + + + Reconciled + + ) : ( + + + Mismatch (${Math.abs(recon.difference).toFixed(2)}M) + + )} +
+
+
+
+ )} + + {/* TAB 3: HO BULK MTM PRICING TOOL */} + {activeTab === 'mtm_pricing' && isHO && ( +
+
+

+ + Head Office Mark-to-Market (MTM) Revaluation Feed +

+

+ Input MTM prices (% of par) or Yields for specific ISIN securities. All branches holding securities with matching ISIN will be updated and revalued automatically. +

+ + {mtmSuccessMsg && ( +
+ + {mtmSuccessMsg} +
+ )} + +
+
+ + setBulkIsin(e.target.value)} + required + className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-cyan-300 text-sm focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + setBulkMtmPrice(e.target.value)} + required + className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-emerald-400 text-sm focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + setBulkMtmYield(e.target.value)} + className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-purple-300 text-sm focus:outline-none focus:border-cyan-500" + /> +
+ +
+ +
+
+
+ + {/* Preset MTM Market Feed Quotes */} +
+

+ Bloomberg / Market Benchmark Reference Quotes (30-Jun-26 Feed) + Click quote to auto-fill +

+ +
+ {[ + { isin: 'XS2232319638', name: 'GOP EuroBond 2031', price: 100.38, yield: 7.28 }, + { isin: 'XS1729875598', name: 'GOP EuroBond 2027', price: 100.50, yield: 6.50 }, + { isin: 'XS3353874145', name: 'GOP EuroBond 2029', price: 99.75, yield: 7.07 }, + { isin: 'XS2419405274', name: 'PAKISTAN SUKUK 7.95%', price: 99.99, yield: 7.95 }, + { isin: 'BD927251100', name: 'Bangladesh 10Y T-Bond', price: 98.45, yield: 9.71 }, + { isin: 'BD934481203', name: 'Bangladesh 20Y T-Bond', price: 109.90, yield: 10.27 }, + ].map((feed) => ( + + ))} +
+
+
+ )} + + {/* MODAL: ADD / EDIT SECURITY */} + {isModalOpen && editingSecurity && ( +
+
+ + +

+ {editingSecurity.id ? 'Edit Investment Security' : 'Add Investment Security'} +

+

+ Enter complete security details for portfolio tracking & reconciliation. +

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + setEditingSecurity({ ...editingSecurity, isin: e.target.value })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-cyan-300 text-sm" + /> +
+ +
+ + setEditingSecurity({ ...editingSecurity, securityType: e.target.value })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm" + /> +
+ +
+ + +
+ +
+ + setEditingSecurity({ ...editingSecurity, faceValue: Number(e.target.value) })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-100 text-sm" + /> +
+ +
+ + + setEditingSecurity({ ...editingSecurity, amountInvested: Number(e.target.value) }) + } + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-emerald-400 text-sm" + /> +
+ +
+ + setEditingSecurity({ ...editingSecurity, couponRate: Number(e.target.value) })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-200 text-sm" + /> +
+ +
+ + + setEditingSecurity({ ...editingSecurity, originalPrice: Number(e.target.value) }) + } + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-200 text-sm" + /> +
+ +
+ + setEditingSecurity({ ...editingSecurity, purchaseDate: e.target.value })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm" + /> +
+ +
+ + setEditingSecurity({ ...editingSecurity, maturityDate: e.target.value })} + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm" + /> +
+
+ + {isHO && ( +
+
Head Office Valuation (MTM)
+
+
+ + + setEditingSecurity({ ...editingSecurity, mtmPrice: Number(e.target.value) }) + } + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-700 font-mono text-cyan-300 text-xs" + /> +
+
+ + + setEditingSecurity({ ...editingSecurity, mtmYield: Number(e.target.value) }) + } + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-700 font-mono text-purple-300 text-xs" + /> +
+
+
+ )} + +
+ + +
+
+
+
+ )} +
+ ); +}; diff --git a/src/components/LiquidityGapView.tsx b/src/components/LiquidityGapView.tsx new file mode 100644 index 0000000..b7df2b1 --- /dev/null +++ b/src/components/LiquidityGapView.tsx @@ -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 = ({ + currentUser, + activePeriod, +}) => { + const isAdmin = currentUser.role === 'admin'; + const initialBranch = currentUser.branchId || 'all'; + + const [selectedBranchId, setSelectedBranchId] = useState(initialBranch); + const [horizonMode, setHorizonMode] = useState<'weekly' | 'regulatory'>('weekly'); + + const [placements, setPlacements] = useState([]); + const [borrowings, setBorrowings] = useState([]); + const [investments, setInvestments] = useState([]); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState(null); + + // Custom manual inputs for line items not auto-derived from deals + const [manualInputs, setManualInputs] = useState>>({ + 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 = {}; + + 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 ( +
+ {/* Top Header Card matching the Liquidity Gap Report screenshot layout */} +
+
+
+
+ +
+
+

+ LIQUIDITY GAP REPORT & MATURITY PROFILE LADDER +

+

+ Asset-Liability Management (ALM) Structural Mismatch & Liquidity Exposure Engine +

+
+
+ +
+ {/* Horizon Mode Toggle */} +
+ + +
+ + {/* Save Custom Inputs */} + + + {/* Refresh */} + +
+
+ + {/* Reporting Metadata Header Grid (matching screenshot) */} +
+
+ Country / Branch Name: + +
+ +
+ Dated: + {activePeriod} +
+ +
+ Currency Unit: + + USD IN MILLION + +
+
+
+ + {saveMessage && ( +
+ + {saveMessage} +
+ )} + + {/* Main Liquidity Gap Matrix Table (Exact visual replica of screenshot) */} +
+
+ + {/* Table Header Row: Liquidity Gap Report Title + Date Buckets */} + + + + {buckets.map((b) => ( + + ))} + + + + + {/* SECTION 1: OUTFLOWS (-) [RED HEADING] */} + + + + + {/* Row 1.1: Customer Deposit outflows (-) */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* Row 1.2: Other Bank/FI Borrowing (-) */} + + + {buckets.map((_, idx) => { + const val = matrix.otherBankBorrowings[idx]; + return ( + + ); + })} + + + {/* Row 1.3: HOT Borrowing (-) */} + + + {buckets.map((_, idx) => { + const val = matrix.hotBorrowings[idx]; + return ( + + ); + })} + + + {/* Row 1.4: NBP Branch Borrowing (-) */} + + + {buckets.map((_, idx) => { + const val = matrix.branchBorrowings[idx]; + return ( + + ); + })} + + + {/* Row 1.5: Other Expected Outflows (-) */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* TOTAL OUTFLOWS ROW */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* SECTION 2: INFLOWS (+) [GREEN HEADING] */} + + + + + {/* Row 2.1: Other Bank/FI/Placement/Lending (+) */} + + + {buckets.map((_, idx) => { + const val = matrix.otherBankPlacements[idx]; + return ( + + ); + })} + + + {/* Row 2.2: HOT Placement/Lending (+) */} + + + {buckets.map((_, idx) => { + const val = matrix.hotPlacements[idx]; + return ( + + ); + })} + + + {/* Row 2.3: NBP Branch Placement/Lending (+) */} + + + {buckets.map((_, idx) => { + const val = matrix.branchPlacements[idx]; + return ( + + ); + })} + + + {/* Row 2.4: Maturing Investments (+) */} + + + {buckets.map((_, idx) => { + const val = matrix.maturingInvestments[idx]; + return ( + + ); + })} + + + {/* Row 2.5: Advances (+) (Excluding Trade Assets) */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* Row 2.6: Trade Assets (+) */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* Row 2.7: Other Expected Inflows (+) */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* TOTAL INFLOWS ROW */} + + + {buckets.map((_, idx) => ( + + ))} + + + {/* SECTION 3: GAP ROW (NET PERIODIC MISMATCH) */} + + + {buckets.map((_, idx) => { + const gVal = matrix.gap[idx]; + return ( + + ); + })} + + + {/* SECTION 4: CUMULATIVE GAP ROW (BLACK/GOLD HEADER LIKE SCREENSHOT) */} + + + {buckets.map((_, idx) => { + const cVal = matrix.cumulativeGap[idx]; + return ( + + ); + })} + + +
+ Liquidity Gap Report + +
{b.label}
+ {b.subLabel &&
{b.subLabel}
} +
+ + OUTFLOWS (-) +
+ Customer Deposit outflows (-) + + 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" + /> +
+ Other Bank/FI Borrowing (-) + 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) : '-'} +
+ HOT Borrowing (-) + 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) : '-'} +
+ NBP Branch Borrowing (-) + 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) : '-'} +
+ Other Expected Outflows (-) + + 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" + /> +
+ TOTAL OUTFLOWS (-) + + {matrix.totalOutflows[idx] > 0 ? matrix.totalOutflows[idx].toFixed(2) : '-'} +
+ + INFLOWS (+) +
+ Other Bank/FI/Placement/Lending (+) + 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) : '-'} +
+ HOT Placement/Lending (+) + 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) : '-'} +
+ NBP Branch Placement/Lending (+) + 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) : '-'} +
+ Maturing Investments (+) + 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) : '-'} +
+ Advances (+) (Excluding Trade Assets) + + 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" + /> +
+ Trade Assets (+) + + 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" + /> +
+ Other Expected Inflows (+) + + 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" + /> +
+ TOTAL INFLOWS (+) + + {matrix.totalInflows[idx] > 0 ? matrix.totalInflows[idx].toFixed(2) : '-'} +
+ NET + GAP + 0 ? 'text-emerald-400' : gVal < 0 ? 'text-rose-400' : 'text-slate-500' + }`} + > + {gVal !== 0 ? (gVal > 0 ? `+${gVal.toFixed(2)}` : gVal.toFixed(2)) : '-'} +
+ CUMULATIVE GAP + 0 ? 'text-emerald-300' : cVal < 0 ? 'text-rose-400' : 'text-amber-400' + }`} + > + {cVal !== 0 ? (cVal > 0 ? `+${cVal.toFixed(2)}` : cVal.toFixed(2)) : '-'} +
+
+ + {/* Footer Legend matching screenshot */} +
+
+ HOT = Head Office Treasury + NBP = National Bank Network + All figures auto-derived from Placements, Borrowings & Investments Portfolios. +
+ Unit: Million USD +
+
+ + {/* Drilldown Modal / Drawer when a non-zero deal cell is clicked */} + {inspectedCell && ( +
+
+
+
+

+ + Deals Breakdown: {inspectedCell.rowLabel} +

+

+ Maturity Bucket: {inspectedCell.bucketLabel} +

+
+ +
+ +
+ {inspectedCell.items.length === 0 ? ( +
+ No individual deal records mapped to this specific cell. +
+ ) : ( + inspectedCell.items.map((item, index) => ( +
+
+
{item.title}
+
+ {item.category} + Maturity: {item.maturityDate} + {item.rate !== undefined && Rate: {item.rate}%} +
+
+
+
+ USD {item.amountUsdMn.toFixed(2)} Mn +
+
+
+ )) + )} +
+ +
+ + Total Bucket Items: {inspectedCell.items.length} + + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..03c2e9e --- /dev/null +++ b/src/components/Navbar.tsx @@ -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> = ({ + 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 ( + + ); +}; + +// Top Header Bar Component with Mobile Navigation Drawer +export const HeaderBar: React.FC = ({ + 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 ( +
+
+ {/* Mobile Menu Toggle Button */} + + + {/* Brand Badge */} +
+
B
+ EQUITYCORE +
+ +

+ {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'} +

+ +
+ + Period: + + + {/* Date Select Dropdown */} + +
+
+ +
+
+ +
+ + PERIOD CLOSES IN + + + {String(countdown.days).padStart(2, '0')}d : {String(countdown.hours).padStart(2, '0')}h : {String(countdown.mins).padStart(2, '0')}m + +
+
+ +
+ {onOpenAuthModal && ( + + )} + + +
+
+ + {/* Mobile Drawer Overlay */} + {mobileMenuOpen && ( +
+
+ Navigation Matrix + {currentUser.name} +
+ +
+ {isAdmin && ( + + )} + + + + + + + + {isAdmin && ( + <> + + + + + + + )} +
+ + {/* Quick User Identity Switcher for Mobile */} +
+ + + {showUserMenu && ( +
+ {allUsers.filter((u) => u.approved).map((user) => ( + + ))} +
+ )} +
+
+ )} +
+ ); +}; + +// Backward-compatible export +export const Navbar = HeaderBar; diff --git a/src/components/PlacementsBorrowingsView.tsx b/src/components/PlacementsBorrowingsView.tsx new file mode 100644 index 0000000..49b5f12 --- /dev/null +++ b/src/components/PlacementsBorrowingsView.tsx @@ -0,0 +1,1240 @@ +import React, { useState, useEffect } from 'react'; +import { + User, + BranchId, + BRANCHES_LIST, + PlacementRecord, + BorrowingRecord, + PlacementsBorrowingsReconciliation, + InterBranchPairwiseMismatch, +} from '../types'; +import { + ArrowLeftRight, + Plus, + Trash2, + Edit2, + CheckCircle2, + AlertTriangle, + RefreshCw, + Search, + Building2, + Coins, + ShieldAlert, + ArrowUpRight, + ArrowDownLeft, + Calendar, + Layers, + Sparkles, + Info +} from 'lucide-react'; + +interface PlacementsBorrowingsViewProps { + currentUser: User; + activePeriod: string; +} + +export const PlacementsBorrowingsView: React.FC = ({ + currentUser, + activePeriod, +}) => { + const isAdmin = currentUser.role === 'admin'; + const initialBranch = currentUser.branchId || 'japan'; + + const [selectedBranchId, setSelectedBranchId] = useState(isAdmin ? 'all' : initialBranch); + const [f48Rate, setF48Rate] = useState(278.16); + const [activeTab, setActiveTab] = useState<'placements' | 'borrowings' | 'pairwise' | 'bs_reconcile'>('placements'); + + const [placements, setPlacements] = useState([]); + const [borrowings, setBorrowings] = useState([]); + const [reconciliations, setReconciliations] = useState([]); + const [pairwiseMismatches, setPairwiseMismatches] = useState([]); + + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [syncStatus, setSyncStatus] = useState<{ message: string; type: 'success' | 'error' } | null>(null); + + // Modal State + const [modalType, setModalType] = useState<'placement' | 'borrowing' | null>(null); + const [editingRecord, setEditingRecord] = useState(null); + + // Form State + const [formData, setFormData] = useState({ + id: '', + branchId: (isAdmin ? (selectedBranchId === 'all' ? 'japan' : selectedBranchId) : currentUser.branchId || 'japan') as BranchId, + counterpartyCountryBranch: 'Japan / Tokyo', + counterpartyName: '', + counterpartyBranchId: '' as BranchId | '', + type: 'Inter Branch' as 'Inter Branch' | 'Inter Bank' | 'Head Office', + approvedLimitMn: 5.0, + currency: 'USD', + amountActualMn: 1.0, + ratePercent: 4.17, + placementDate: new Date().toISOString().split('T')[0], + maturityDate: new Date(Date.now() + 30 * 24 * 3600 * 1000).toISOString().split('T')[0], + }); + + const selectedBranchInfo = BRANCHES_LIST.find((b) => b.id === (selectedBranchId === 'all' ? (currentUser.branchId || 'japan') : selectedBranchId)) || BRANCHES_LIST[0]; + + useEffect(() => { + if (!isAdmin && currentUser.branchId) { + setSelectedBranchId(currentUser.branchId); + } + }, [currentUser, isAdmin]); + + useEffect(() => { + fetchData(); + }, [activePeriod, selectedBranchId, currentUser]); + + const fetchData = async () => { + setLoading(true); + try { + const branchParam = !isAdmin ? currentUser.branchId : (selectedBranchId === 'all' ? 'all' : selectedBranchId); + const [plcRes, borRes, reconRes] = await Promise.all([ + fetch(`/api/placements?period=${encodeURIComponent(activePeriod)}&branchId=${branchParam || ''}`), + fetch(`/api/borrowings?period=${encodeURIComponent(activePeriod)}&branchId=${branchParam || ''}`), + fetch(`/api/placements-borrowings/reconciliation?period=${encodeURIComponent(activePeriod)}`), + ]); + + if (plcRes.ok) { + const data = await plcRes.json(); + setPlacements(data.placements || []); + } + if (borRes.ok) { + const data = await borRes.json(); + setBorrowings(data.borrowings || []); + } + if (reconRes.ok) { + const data = await reconRes.json(); + setReconciliations(data.reconciliations || []); + setPairwiseMismatches(data.pairwiseMismatches || []); + } + } catch (err) { + console.error('Failed to fetch placements and borrowings:', err); + } finally { + setLoading(false); + } + }; + + const handleOpenAddModal = (type: 'placement' | 'borrowing') => { + setModalType(type); + setEditingRecord(null); + setFormData({ + id: '', + branchId: selectedBranchId, + counterpartyCountryBranch: `${selectedBranchInfo.country} / ${selectedBranchInfo.name}`, + counterpartyName: type === 'placement' ? 'NBP EPZ KARACHI' : 'NBP EPZ KARACHI', + counterpartyBranchId: 'epz', + type: 'Inter Branch', + approvedLimitMn: 10.0, + currency: 'USD', + amountActualMn: 1.0, + ratePercent: 4.17, + placementDate: '2026-07-24', + maturityDate: '2026-08-24', + }); + }; + + const handleOpenEditModal = (type: 'placement' | 'borrowing', record: any) => { + setModalType(type); + setEditingRecord(record); + setFormData({ + id: record.id, + branchId: record.branchId, + counterpartyCountryBranch: record.counterpartyCountryBranch || '', + counterpartyName: record.counterpartyName || '', + counterpartyBranchId: record.counterpartyBranchId || '', + type: record.type || 'Inter Branch', + approvedLimitMn: record.approvedLimitMn || 0, + currency: record.currency || 'USD', + amountActualMn: record.amountActualMn || 0, + ratePercent: record.ratePercent || 0, + placementDate: record.placementDate || record.borrowingDate || '', + maturityDate: record.maturityDate || '', + }); + }; + + const handleDeleteRecord = async (type: 'placement' | 'borrowing', id: string) => { + if (!window.confirm(`Are you sure you want to delete this ${type} deal?`)) return; + try { + const endpoint = type === 'placement' ? `/api/placements/${id}` : `/api/borrowings/${id}`; + const res = await fetch(endpoint, { method: 'DELETE' }); + if (res.ok) { + fetchData(); + } + } catch (err) { + console.error('Delete failed:', err); + } + }; + + const handleSaveModal = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const endpoint = modalType === 'placement' ? '/api/placements' : '/api/borrowings'; + const payload = { + ...formData, + period: activePeriod, + f48Rate, + branchId: selectedBranchId, + amountActualMn: Number(formData.amountActualMn), + ratePercent: Number(formData.ratePercent), + approvedLimitMn: Number(formData.approvedLimitMn), + [modalType === 'placement' ? 'placementDate' : 'borrowingDate']: formData.placementDate, + }; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (res.ok) { + setModalType(null); + fetchData(); + } + } catch (err) { + console.error('Save failed:', err); + } + }; + + const handleSyncToBalanceSheet = async (branchIdToSync: BranchId) => { + try { + setSyncStatus(null); + const res = await fetch('/api/placements-borrowings/sync-balance-sheet', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ period: activePeriod, branchId: branchIdToSync }), + }); + + if (res.ok) { + const data = await res.json(); + setSyncStatus({ + type: 'success', + message: data.message || `Balance Sheet successfully updated for ${branchIdToSync}!`, + }); + fetchData(); + } else { + setSyncStatus({ + type: 'error', + message: 'Failed to sync with Balance Sheet.', + }); + } + } catch (err) { + console.error('Sync failed:', err); + } + }; + + // Branch filtered data + const filteredPlacements = placements + .filter((p) => (!isAdmin ? p.branchId === currentUser.branchId : (selectedBranchId === 'all' || p.branchId === selectedBranchId))) + .filter((p) => + p.counterpartyName.toLowerCase().includes(searchQuery.toLowerCase()) || + p.counterpartyCountryBranch.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const filteredBorrowings = borrowings + .filter((b) => (!isAdmin ? b.branchId === currentUser.branchId : (selectedBranchId === 'all' || b.branchId === selectedBranchId))) + .filter((b) => + b.counterpartyName.toLowerCase().includes(searchQuery.toLowerCase()) || + b.counterpartyCountryBranch.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Totals + const totalPlacementUsd = filteredPlacements.reduce((sum, p) => sum + (p.equivalentUsdMn || 0), 0); + const totalPlacementPkr = totalPlacementUsd * f48Rate; + + const totalBorrowingUsd = filteredBorrowings.reduce((sum, b) => sum + (b.equivalentUsdMn || 0), 0); + const totalBorrowingPkr = totalBorrowingUsd * f48Rate; + + const currentRecon = reconciliations.find((r) => r.branchId === (selectedBranchId === 'all' ? (currentUser.branchId || 'japan') : selectedBranchId)); + + return ( +
+ {/* High Density Top Banner Header */} +
+
+
+ +
+
+
+

+ Inter-Branch & Inter-Bank Portfolio Engine +

+ + {!isAdmin ? `${currentUser.branchId?.toUpperCase()} SCOPE` : 'HEAD OFFICE MATRIX'} + +
+

+ Reporting Period: {activePeriod} • + Institutional Mark-to-Market & Liquidity Ledger +

+
+
+ + {/* Top Controls: Branch Switcher & F-48 Exchange Rate */} +
+ {/* Branch Selector */} + {isAdmin ? ( +
+ + Branch: + +
+ ) : ( +
+ + Branch Scope: {selectedBranchInfo.name} ({selectedBranchInfo.code}) +
+ )} + + {/* F-48 Rate Input */} +
+ F-48 Rate: + setF48Rate(Number(e.target.value) || 278.16)} + className="w-20 bg-slate-900 text-amber-300 px-1.5 py-0.5 rounded text-xs font-mono font-bold border border-slate-700 focus:border-amber-400 focus:outline-none text-right" + /> + PKR/USD +
+ + {/* Refresh Button */} + +
+
+ + {/* KPI Cards Strip - High Density Financial Metrics */} +
+ {/* Total Placements */} +
+
+ + Placements Portfolio + + + ASSETS + +
+
+
+ USD {totalPlacementUsd.toFixed(2)} Mn +
+
+ PKR {(totalPlacementPkr).toLocaleString(undefined, { maximumFractionDigits: 2 })} Mn +
+
+
+ Inter-Branch: ${currentRecon?.placementsNetworkTotal || 0}Mn + Inter-Bank: ${currentRecon?.placementsOutsideTotal || 0}Mn +
+
+ + {/* Total Borrowings */} +
+
+ + Borrowings Portfolio + + + LIABILITIES + +
+
+
+ USD {totalBorrowingUsd.toFixed(2)} Mn +
+
+ PKR {(totalBorrowingPkr).toLocaleString(undefined, { maximumFractionDigits: 2 })} Mn +
+
+
+ Inter-Branch: ${currentRecon?.borrowingsNetworkTotal || 0}Mn + Inter-Bank: ${currentRecon?.borrowingsOutsideTotal || 0}Mn +
+
+ + {/* Net Inter-Network Exposure */} +
+
+ + Net Liquidity Position + + + NET EXPOSURE + +
+
+
= totalBorrowingUsd ? 'text-cyan-300' : 'text-rose-400'}`}> + USD {(totalPlacementUsd - totalBorrowingUsd).toFixed(2)} Mn +
+
+ {totalPlacementUsd >= totalBorrowingUsd ? 'Net Lender' : 'Net Borrower'} +
+
+
+ Deals Count: {filteredPlacements.length + filteredBorrowings.length} + F-48 FX Factor: {f48Rate} +
+
+ + {/* Balance Sheet Reconciliation Status */} +
+
+ + B/S Reconciliation + + + {currentRecon?.isPlacementMatched && currentRecon?.isBorrowingMatched ? 'MATCHED' : 'DISCREPANCY'} + +
+
+
+ Plc Diff: + ${currentRecon?.placementDifference || 0}Mn + +
+
+ Bor Diff: + ${currentRecon?.borrowingDifference || 0}Mn + +
+
+
+ {selectedBranchInfo.name} Status + +
+
+
+ + {/* Sync Alert Banner if triggered */} + {syncStatus && ( +
+
+ + {syncStatus.message} +
+ +
+ )} + + {/* Navigation Tabs Bar */} +
+
+ + + + + + + +
+ + {/* Search & Action Controls */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 pr-3 py-1 bg-slate-900 border border-slate-700 rounded-lg text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:border-cyan-400 w-48 sm:w-60" + /> +
+ + {(activeTab === 'placements' || activeTab === 'borrowings') && ( + + )} +
+
+ + {/* TAB 1: PLACEMENTS SCHEDULE SHEET */} + {activeTab === 'placements' && ( +
+ {/* Header Bar matching Excel screenshot */} +
+
+

+ INTER-BRANCH AND INTER-BANK PLACEMENTS — Placement Report +

+
+ Branch Name: {selectedBranchInfo.name} Branch • + Date of Reporting: {activePeriod} • + F-48 Rate: {f48Rate} +
+
+ + All the data is to be provided in Million + +
+ + {/* High Density Table */} +
+ + + + + + + + + + + + + + + + + + + + + {filteredPlacements.length === 0 ? ( + + + + ) : ( + filteredPlacements.map((plc) => ( + + + + + + + + + + + + + + + + + )) + )} + + + {/* Total Summary Row matching Excel report */} + {filteredPlacements.length > 0 && ( + + + + + + + + + )} +
Country/BranchPlacement With / Counter PartyTypeApproved Limit (Mn)CurrencyAmount (Actual Mn)Equivalent USD MnEquivalent (PKR Mn)Rate (%)Placement DateDate of MaturityTenor (Days)Limit BreachedActions
+ No placement deals recorded for {selectedBranchInfo.name} Branch in {activePeriod}. Click "Add New Placement" above. +
+ {plc.counterpartyCountryBranch} + + {plc.counterpartyName} + + + {plc.type} + + + {plc.approvedLimitMn ? `$${plc.approvedLimitMn.toFixed(2)}` : 'N/A'} + + {plc.currency} + + {plc.amountActualMn.toLocaleString(undefined, { minimumFractionDigits: 2 })} + + ${plc.equivalentUsdMn.toFixed(2)} + + PKR {plc.equivalentPkrMn.toLocaleString(undefined, { minimumFractionDigits: 2 })} + + {plc.ratePercent}% + + {plc.placementDate} + + {plc.maturityDate} + + {plc.tenorDays} + + {plc.limitBreached ? ( + + YES + + ) : ( + + NO + + )} + +
+ + +
+
+ TOTAL PLACEMENTS ({selectedBranchInfo.name}): + + ${totalPlacementUsd.toFixed(2)} Mn + + PKR {totalPlacementPkr.toLocaleString(undefined, { minimumFractionDigits: 2 })} Mn + + Reconciled against Balance Sheet Asset Line Item 'Placement Network' +
+
+
+ )} + + {/* TAB 2: BORROWINGS SCHEDULE SHEET */} + {activeTab === 'borrowings' && ( +
+ {/* Header Bar matching Excel screenshot */} +
+
+

+ INTER-BRANCH AND INTER-BANK BORROWINGS — Borrowing Report +

+
+ Branch Name: {selectedBranchInfo.name} Branch • + Date of Reporting: {activePeriod} • + F-48 Rate: {f48Rate} +
+
+ + All the data is to be provided in Million + +
+ + {/* High Density Table */} +
+ + + + + + + + + + + + + + + + + + + {filteredBorrowings.length === 0 ? ( + + + + ) : ( + filteredBorrowings.map((bor) => ( + + + + + + + + + + + + + + + )) + )} + + + {/* Total Summary Row matching Excel report */} + {filteredBorrowings.length > 0 && ( + + + + + + + + + )} +
Country/BranchBorrowing from / Counter PartyTypeCurrencyAmount Borrowed (Actual Currency)Amount in Equivalent USD MnEquivalent (PKR Mn)Rate (%)Borrowing DateDate of MaturityTenor (Days)Actions
+ No borrowing deals recorded for {selectedBranchInfo.name} Branch in {activePeriod}. Click "Add New Borrowing" above. +
+ {bor.counterpartyCountryBranch} + + {bor.counterpartyName} + + + {bor.type} + + + {bor.currency} + + {bor.amountActualMn.toLocaleString(undefined, { minimumFractionDigits: 2 })} + + ${bor.equivalentUsdMn.toFixed(2)} + + PKR {bor.equivalentPkrMn.toLocaleString(undefined, { minimumFractionDigits: 2 })} + + {bor.ratePercent}% + + {bor.borrowingDate} + + {bor.maturityDate} + + {bor.tenorDays} + +
+ + +
+
+ TOTAL BORROWINGS ({selectedBranchInfo.name}): + + ${totalBorrowingUsd.toFixed(2)} Mn + + PKR {totalBorrowingPkr.toLocaleString(undefined, { minimumFractionDigits: 2 })} Mn + + Reconciled against Balance Sheet Liability Line Item 'Borrowing Network' +
+
+
+ )} + + {/* TAB 3: PAIRWISE INTER-BRANCH RECONCILIATION */} + {activeTab === 'pairwise' && ( +
+
+
+
+

+ Pairwise Inter-Branch Reconciliation Matrix +

+

+ Verifies that Branch A's reported Placement with Branch B equals Branch B's reported Borrowing from Branch A. +

+
+ +
+ + Matched + + + Mismatch + +
+
+ +
+ + + + + + + + + + + + + + {pairwiseMismatches.length === 0 ? ( + + + + ) : ( + pairwiseMismatches.map((m) => ( + + + + + + + + + + )) + )} + +
Placing Branch (Lender)Borrowing Branch (Borrower)Placing Branch Amount ($Mn)Borrowing Branch Amount ($Mn)Variance ($Mn)StatusReconciliation Details & Audit Trail
+ No active inter-branch positions detected between network branches for period {activePeriod}. +
+ {m.placingBranchName} + + {m.borrowingBranchName} + + ${m.placementUsdMn.toFixed(2)} + + ${m.borrowingUsdMn.toFixed(2)} + 0 ? 'text-rose-400' : 'text-emerald-400' + }`}> + ${m.differenceUsdMn.toFixed(2)} + + {m.status === 'matched' ? ( + + PASS + + ) : ( + + FLAGGED + + )} + + {m.details} +
+
+
+
+ )} + + {/* TAB 4: BALANCE SHEET AUTO-CARRY & RECONCILIATION */} + {activeTab === 'bs_reconcile' && ( +
+
+
+
+

+ Placements & Borrowings Schedule vs Balance Sheet Audit +

+

+ Ensures all schedule totals match the corresponding Balance Sheet asset and liability line items. +

+
+ + {isAdmin && ( + + )} +
+ +
+ + + + + + + + + + + + + + + + {reconciliations.map((r) => { + const isAllPass = r.isPlacementMatched && r.isBorrowingMatched; + return ( + + + + + + + + + + + + ); + })} + +
Branch NamePlacement Schedule ($Mn)B/S Placement Item ($Mn)Plc DiffBorrowing Schedule ($Mn)B/S Borrowing Item ($Mn)Bor DiffStatusAction
+ {r.branchName} + {r.branchId === selectedBranchId && ( + ACTIVE + )} + + ${r.placementsTotal.toFixed(2)} + + ${(r.bsPlacementNetwork + r.bsPlacementOutside).toFixed(2)} + + ${r.placementDifference.toFixed(2)} + + ${r.borrowingsTotal.toFixed(2)} + + ${(r.bsBorrowingNetwork + r.bsBorrowingOutside).toFixed(2)} + + ${r.borrowingDifference.toFixed(2)} + + {isAllPass ? ( + + MATCHED + + ) : ( + + MISMATCH + + )} + + +
+
+
+
+ )} + + {/* ADD / EDIT DEAL MODAL */} + {modalType && ( +
+
+
+

+ {modalType === 'placement' ? ( + + ) : ( + + )} + {editingRecord ? 'Edit' : 'Add New'} {modalType === 'placement' ? 'Placement Deal' : 'Borrowing Deal'} +

+ +
+ +
+
+
+ + setFormData({ ...formData, counterpartyName: e.target.value })} + placeholder="e.g. NBP EPZ KARACHI" + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 focus:outline-none focus:border-cyan-400" + /> +
+ +
+ + setFormData({ ...formData, counterpartyCountryBranch: e.target.value })} + placeholder="e.g. Japan / Tokyo" + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 focus:outline-none focus:border-cyan-400" + /> +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + setFormData({ ...formData, amountActualMn: Number(e.target.value) })} + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-cyan-400" + /> +
+ +
+ + setFormData({ ...formData, ratePercent: Number(e.target.value) })} + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-cyan-400" + /> +
+ + {modalType === 'placement' && ( +
+ + setFormData({ ...formData, approvedLimitMn: Number(e.target.value) })} + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-cyan-400" + /> +
+ )} +
+ +
+
+ + setFormData({ ...formData, placementDate: e.target.value })} + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-cyan-400" + /> +
+ +
+ + setFormData({ ...formData, maturityDate: e.target.value })} + className="w-full bg-slate-950 border border-slate-700 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-cyan-400" + /> +
+
+ + {/* Calculated preview */} +
+ + USD Eq: ${(formData.amountActualMn * (formData.currency === 'USD' ? 1 : 1 / f48Rate)).toFixed(2)} Mn + + + PKR Eq: PKR {(formData.amountActualMn * (formData.currency === 'USD' ? f48Rate : 1)).toFixed(2)} Mn + +
+ +
+ + +
+
+
+
+ )} +
+ ); +}; diff --git a/src/data/investmentSeedData.ts b/src/data/investmentSeedData.ts new file mode 100644 index 0000000..7760a2e --- /dev/null +++ b/src/data/investmentSeedData.ts @@ -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, + }, +]; diff --git a/src/data/placementsBorrowingsSeedData.ts b/src/data/placementsBorrowingsSeedData.ts new file mode 100644 index 0000000..cab8ad5 --- /dev/null +++ b/src/data/placementsBorrowingsSeedData.ts @@ -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, + } +]; diff --git a/src/data/seedData.ts b/src/data/seedData.ts new file mode 100644 index 0000000..fa769c9 --- /dev/null +++ b/src/data/seedData.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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, + commentsMap?: Record +): 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)), + }; + }); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..8e302cb --- /dev/null +++ b/src/index.css @@ -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; +} + + diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..080dac3 --- /dev/null +++ b/src/main.tsx @@ -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( + + + , +); diff --git a/src/services/accountingEngine.ts b/src/services/accountingEngine.ts new file mode 100644 index 0000000..9e6dda2 --- /dev/null +++ b/src/services/accountingEngine.ts @@ -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 = { + 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 = { + 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 = { + 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 + ): { 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, + }; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a7924b7 --- /dev/null +++ b/src/types.ts @@ -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' }, +]; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d88f175 --- /dev/null +++ b/tsconfig.json @@ -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 + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..fc23e76 --- /dev/null +++ b/vite.config.ts @@ -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 : {}, + }, + }; +});