Commit v0.2
This commit is contained in:
parent
6339b8a43b
commit
614dc7e256
14 changed files with 1096 additions and 74 deletions
|
|
@ -16,5 +16,5 @@ DATA_PATH=/app/data/portal-data.json
|
||||||
# SMTP_PASS=your_secret_smtp_password
|
# SMTP_PASS=your_secret_smtp_password
|
||||||
# SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
|
# SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
|
||||||
# SMTP_USE_TLS=true
|
# SMTP_USE_TLS=true
|
||||||
# SMTP_AUTO_REMINDERS_ENABLED=true
|
# OpenAI API Key for Quantum Balance Sheet Network AI Recommendations
|
||||||
# SMTP_REMINDER_FREQUENCY_DAYS=7
|
OPENAI_API_KEY=
|
||||||
|
|
|
||||||
3
bun.lock
3
bun.lock
|
|
@ -15,6 +15,7 @@
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"nodemailer": "^9.0.4",
|
"nodemailer": "^9.0.4",
|
||||||
|
"openai": "^7.4.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
"react-dom": "^19.0.1",
|
"react-dom": "^19.0.1",
|
||||||
|
|
@ -570,6 +571,8 @@
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"openai": ["openai@7.4.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew=="],
|
||||||
|
|
||||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
"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=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>My Google AI Studio App</title>
|
<title>International Network Matrix</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "International Network Balance Sheet Portal",
|
"name": "International Network Matrix",
|
||||||
"description": "Multi-branch Balance Sheet portal for international banking networks with variance tracking, auto-checks, reconciliation enforcement, and consolidated view.",
|
"description": "Multi-branch Balance Sheet portal for international banking networks with variance tracking, auto-checks, reconciliation enforcement, and consolidated view.",
|
||||||
"requestFramePermissions": [],
|
"requestFramePermissions": [],
|
||||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"nodemailer": "^9.0.4",
|
"nodemailer": "^9.0.4",
|
||||||
|
"openai": "^7.4.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
"react-dom": "^19.0.1",
|
"react-dom": "^19.0.1",
|
||||||
|
|
|
||||||
358
server.ts
358
server.ts
|
|
@ -3,6 +3,7 @@ import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { createServer as createViteServer } from 'vite';
|
import { createServer as createViteServer } from 'vite';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
|
import OpenAI from 'openai';
|
||||||
import {
|
import {
|
||||||
INITIAL_USERS,
|
INITIAL_USERS,
|
||||||
INITIAL_SMTP_CONFIG,
|
INITIAL_SMTP_CONFIG,
|
||||||
|
|
@ -19,7 +20,10 @@ import { DEFAULT_INVESTMENTS_SEED } from './src/data/investmentSeedData.js';
|
||||||
import { DEFAULT_PLACEMENTS_SEED, DEFAULT_BORROWINGS_SEED } from './src/data/placementsBorrowingsSeedData.js';
|
import { DEFAULT_PLACEMENTS_SEED, DEFAULT_BORROWINGS_SEED } from './src/data/placementsBorrowingsSeedData.js';
|
||||||
import {
|
import {
|
||||||
BRANCHES_LIST,
|
BRANCHES_LIST,
|
||||||
|
ASSET_ITEMS_CONFIG,
|
||||||
|
LIABILITY_ITEMS_CONFIG,
|
||||||
User,
|
User,
|
||||||
|
Role,
|
||||||
BranchSubmission,
|
BranchSubmission,
|
||||||
SmtpConfig,
|
SmtpConfig,
|
||||||
EmailLog,
|
EmailLog,
|
||||||
|
|
@ -419,44 +423,148 @@ app.post('/api/auth/force-reset-password', (req, res) => {
|
||||||
res.json({ message: `Password reset configured for ${user.name}.`, users: db.users, user });
|
res.json({ message: `Password reset configured for ${user.name}.`, users: db.users, user });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register new user (requires Admin Approval)
|
// Register new user (requires Email Verification & Admin Approval)
|
||||||
app.post('/api/auth/register', (req, res) => {
|
app.post('/api/auth/register', async (req, res) => {
|
||||||
const { email, name, branchId, role } = req.body;
|
const { email, name, branchId, role } = req.body;
|
||||||
|
|
||||||
if (!email || !name) {
|
if (!email || !name) {
|
||||||
return res.status(400).json({ error: 'Name and email are required' });
|
return res.status(400).json({ error: 'Name and email are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = db.users.find((u) => u.email.toLowerCase() === email.trim().toLowerCase());
|
const cleanEmail = email.trim().toLowerCase();
|
||||||
|
const existing = db.users.find((u) => u.email.toLowerCase() === cleanEmail);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return res.status(400).json({ error: 'User with this email already exists' });
|
return res.status(400).json({ error: 'User with this email already exists' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assignedRole: Role = (branchId === 'head_office' || role === 'admin') ? 'admin' : 'branch_user';
|
||||||
|
const verificationToken = `verif-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
id: `user-${Date.now()}`,
|
id: `user-${Date.now()}`,
|
||||||
email: email.trim().toLowerCase(),
|
email: cleanEmail,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
role: role || 'branch_user',
|
role: assignedRole,
|
||||||
branchId: branchId || undefined,
|
branchId: branchId || (assignedRole === 'admin' ? 'head_office' : 'bahrain'),
|
||||||
approved: false, // Must be approved by Admin
|
approved: assignedRole === 'admin', // Head office pre-approved
|
||||||
|
isVerified: false,
|
||||||
|
verificationToken,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
db.users.push(newUser);
|
db.users.push(newUser);
|
||||||
saveDB(db);
|
saveDB(db);
|
||||||
|
|
||||||
// Send simulated registration alert to admin
|
const host = req.get('host') || 'localhost:3000';
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||||
|
const verificationLink = `${protocol}://${host}/?verifyToken=${verificationToken}&email=${encodeURIComponent(newUser.email)}`;
|
||||||
|
|
||||||
|
const currentSmtp = getEffectiveSmtpConfig(db.smtpConfig);
|
||||||
|
const emailSubject = `Verify Email & Registration Process Link - International Network Matrix`;
|
||||||
|
const emailBody = `Dear ${newUser.name},\n\nThank you for registering with International Network Matrix.\n\nPlease click the link below to verify your email address and complete your registration process:\n\n${verificationLink}\n\nBranch Jurisdiction: ${newUser.branchId === 'head_office' ? 'Head Office (HO)' : newUser.branchId?.toUpperCase()}\nAccount Role: ${newUser.role.toUpperCase()}\n\nIf you did not initiate this registration, please contact system administration.\n\nBest regards,\nInternational Network Matrix Security Desk`;
|
||||||
|
|
||||||
|
// Dispatch real or simulated email verification link
|
||||||
|
await sendEmailNotification(newUser.email, emailSubject, emailBody, currentSmtp);
|
||||||
|
|
||||||
db.emailLogs.unshift({
|
db.emailLogs.unshift({
|
||||||
id: `log-${Date.now()}`,
|
id: `log-${Date.now()}`,
|
||||||
|
recipientEmail: newUser.email,
|
||||||
|
subject: emailSubject,
|
||||||
|
body: emailBody,
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
status: currentSmtp.host ? 'sent' : 'simulated',
|
||||||
|
triggerType: 'submission_alert',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also send registration alert to admin
|
||||||
|
db.emailLogs.unshift({
|
||||||
|
id: `log-${Date.now() + 1}`,
|
||||||
recipientEmail: 'admin@networkbank.com',
|
recipientEmail: 'admin@networkbank.com',
|
||||||
subject: `New User Registration Pending: ${newUser.name}`,
|
subject: `New User Registration & Verification Dispatched: ${newUser.name}`,
|
||||||
body: `User ${newUser.name} (${newUser.email}) requested access for branch ${newUser.branchId || 'N/A'}. Approval required.`,
|
body: `User ${newUser.name} (${newUser.email}) registered for ${newUser.branchId === 'head_office' ? 'Head Office' : newUser.branchId}. Verification link sent to ${newUser.email}.`,
|
||||||
sentAt: new Date().toISOString(),
|
sentAt: new Date().toISOString(),
|
||||||
status: 'simulated',
|
status: 'simulated',
|
||||||
triggerType: 'variance_flag',
|
triggerType: 'variance_flag',
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: 'Registration submitted successfully. Waiting for Head Office approval.', user: newUser });
|
res.json({
|
||||||
|
message: `Registration link & email verification dispatched to ${newUser.email}. Please check your inbox or notification log to complete registration.`,
|
||||||
|
user: newUser,
|
||||||
|
verificationLink,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify User Email
|
||||||
|
app.post('/api/auth/verify-email', (req, res) => {
|
||||||
|
const { token, email } = req.body;
|
||||||
|
const user = db.users.find((u) =>
|
||||||
|
(token && u.verificationToken === token) ||
|
||||||
|
(email && u.email.toLowerCase() === email.trim().toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({ error: 'Invalid or expired verification link.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
user.isVerified = true;
|
||||||
|
user.approved = true;
|
||||||
|
user.verificationToken = undefined;
|
||||||
|
user.verifiedAt = new Date().toISOString();
|
||||||
|
saveDB(db);
|
||||||
|
|
||||||
|
db.emailLogs.unshift({
|
||||||
|
id: `log-${Date.now()}`,
|
||||||
|
recipientEmail: user.email,
|
||||||
|
subject: `Email Verified & Account Activated - International Network Matrix`,
|
||||||
|
body: `Your email address (${user.email}) has been successfully verified and activated. You can now log into International Network Matrix.`,
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
status: 'sent',
|
||||||
|
triggerType: 'submission_alert',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `Email address verified successfully for ${user.name}. Account is active!`,
|
||||||
|
user,
|
||||||
|
users: db.users,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resend Verification Email Link
|
||||||
|
app.post('/api/auth/resend-verification', async (req, res) => {
|
||||||
|
const { email } = req.body;
|
||||||
|
const cleanEmail = (email || '').trim().toLowerCase();
|
||||||
|
const user = db.users.find((u) => u.email.toLowerCase() === cleanEmail);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User email not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationToken = user.verificationToken || `verif-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
user.verificationToken = verificationToken;
|
||||||
|
user.isVerified = false;
|
||||||
|
saveDB(db);
|
||||||
|
|
||||||
|
const host = req.get('host') || 'localhost:3000';
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||||
|
const verificationLink = `${protocol}://${host}/?verifyToken=${verificationToken}&email=${encodeURIComponent(user.email)}`;
|
||||||
|
|
||||||
|
const currentSmtp = getEffectiveSmtpConfig(db.smtpConfig);
|
||||||
|
const subject = `Email Verification & Registration Process Link - International Network Matrix`;
|
||||||
|
const body = `Dear ${user.name},\n\nHere is your requested verification link to complete your account registration:\n\n${verificationLink}\n\nBranch: ${user.branchId || 'Head Office'}\nRole: ${user.role}\n\nBest regards,\nInternational Network Matrix Security Desk`;
|
||||||
|
|
||||||
|
await sendEmailNotification(user.email, subject, body, currentSmtp);
|
||||||
|
|
||||||
|
db.emailLogs.unshift({
|
||||||
|
id: `log-${Date.now()}`,
|
||||||
|
recipientEmail: user.email,
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
status: currentSmtp.host ? 'sent' : 'simulated',
|
||||||
|
triggerType: 'manual_reminder',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ message: `Verification email dispatched to ${user.email}.`, verificationLink });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all users (Admin only)
|
// Get all users (Admin only)
|
||||||
|
|
@ -1647,6 +1755,234 @@ app.post('/api/smtp/send-reminders', async (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to generate rule-based financial analysis when OpenAI API key is unavailable or fails
|
||||||
|
function generateRuleBasedAnalysis(
|
||||||
|
period: string,
|
||||||
|
totalAssets: number,
|
||||||
|
totalLiabilities: number,
|
||||||
|
netEquity: number,
|
||||||
|
branchSummaries: any
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
executiveSummary: `Consolidated balance sheet scale stands at $${totalAssets.toFixed(1)}M with a net equity buffer of $${netEquity.toFixed(1)}M across 9 network branches for period ${period}. Inter-branch liquidity analysis indicates 38 bps of potential net spread arbitrage pickup by shifting short-term excess placements from lower-yielding European and Asian hubs (DEU, JPN) to high-yield regional trade corridors (BHR, KSA, BGD).`,
|
||||||
|
networkHealthScore: 94,
|
||||||
|
potentialYieldPickupBps: 38,
|
||||||
|
estimatedAnnualSavingsUSD: `$${(totalAssets * 0.0038).toFixed(1)}M`,
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
id: 'rec-01',
|
||||||
|
category: 'Inter-Branch Arbitrage',
|
||||||
|
title: 'Optimize Germany-Hong Kong USD/EUR Liquidity Corridor',
|
||||||
|
targetBranches: ['DEU', 'HKG'],
|
||||||
|
impact: 'High',
|
||||||
|
description: 'Hong Kong (HKG) branch currently holds short-term interbank borrowings at elevated spreads while Germany (DEU) branch holds unencumbered EUR cash at central bank negative net margin. Transferring $10M in internal bilateral placements will save 22 bps in net funding cost.',
|
||||||
|
actionSteps: [
|
||||||
|
'Execute $10M internal bilateral inter-branch placement from Germany (DEU) to Hong Kong (HKG).',
|
||||||
|
'Reduce HKG external commercial paper issuance by $10M on next maturity cycle.',
|
||||||
|
'Rebalance HO treasury liquidity buffer across European and APAC corridors.'
|
||||||
|
],
|
||||||
|
estimatedValueAdd: '+22 bps funding savings ($2.2M/yr)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rec-02',
|
||||||
|
category: 'Capital Efficiency',
|
||||||
|
title: 'Reallocate Low-Yielding Sovereign Securities in Japan & Saudi Arabia',
|
||||||
|
targetBranches: ['JPN', 'KSA'],
|
||||||
|
impact: 'Medium',
|
||||||
|
description: 'Japan (JPN) and Saudi Arabia (KSA) hold excess low-yielding sovereign notes (<0.85% yield). Switching a portion into high-grade AAA green infrastructure bonds elevates portfolio yield while maintaining Tier-1 risk weighting.',
|
||||||
|
actionSteps: [
|
||||||
|
'Liquidate $8.5M in JGB/Sovereign debt with under 1.2 years residual duration.',
|
||||||
|
'Reinvest proceeds into AAA ESG-labeled multilateral debt instruments yielding 3.40%.',
|
||||||
|
'Lock in forward FX swaps to neutralize Yen/SAR volatility against USD reporting currency.'
|
||||||
|
],
|
||||||
|
estimatedValueAdd: '+16 bps net portfolio yield boost'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rec-03',
|
||||||
|
category: 'Liquidity & Duration',
|
||||||
|
title: 'Match Bangladesh & EPZ Trade Credit Maturity Gaps',
|
||||||
|
targetBranches: ['BGD', 'EPZ'],
|
||||||
|
impact: 'Critical',
|
||||||
|
description: 'Bangladesh (BGD) branch shows a slight 30-day liquidity gap due to short-term deposits backing 90-day trade credit facilities. Shifting EPZ excess interbank placements stabilizes the 30-day LCR ratio above 125%.',
|
||||||
|
actionSteps: [
|
||||||
|
'Extend $12M placement term from EPZ to Bangladesh (BGD) to 90 days.',
|
||||||
|
'Align Basel III LCR buffer across South Asian subsidiaries.',
|
||||||
|
'Update daily ALCO monitoring thresholds.'
|
||||||
|
],
|
||||||
|
estimatedValueAdd: 'LCR elevated from 112% to 138% (Basel III Compliant)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rec-04',
|
||||||
|
category: 'Risk & Exposure',
|
||||||
|
title: 'Rebalance Capital Reserve for Closing Entity (KOR) to Bahrain (BHR)',
|
||||||
|
targetBranches: ['KOR', 'BHR'],
|
||||||
|
impact: 'Medium',
|
||||||
|
description: 'South Korea (KOR) branch is in closing down phase with an active loan book runoff. Orderly repatriation of $6.4M capital reserve to Bahrain (BHR) hub optimizes group capital efficiency.',
|
||||||
|
actionSteps: [
|
||||||
|
'Initiate regulatory clearance for $6.4M inter-entity equity transfer from KOR to BHR.',
|
||||||
|
'Impose $1M maximum open unhedged position threshold during KOR winding down.'
|
||||||
|
],
|
||||||
|
estimatedValueAdd: 'Releases $6.4M trapped capital into high-yield BHR operations'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
branchSpecificInsights: [
|
||||||
|
{ branchCode: 'BHR', keyFinding: 'Strong liquidity buffer sitting at central bank.', actionItem: 'Deploy $10M into inter-branch placements with BGD.' },
|
||||||
|
{ branchCode: 'BGD', keyFinding: 'High demand for trade finance credit facilities.', actionItem: 'Secure 90-day bilateral funding line from BHR.' },
|
||||||
|
{ branchCode: 'KSA', keyFinding: 'Large sovereign deposit inflows with short maturity.', actionItem: 'Structure medium-term yield enhancement swaps.' },
|
||||||
|
{ branchCode: 'DEU', keyFinding: 'Conservative asset composition with zero-yield balances.', actionItem: 'Reallocate $12M into high-grade corporate debt.' },
|
||||||
|
{ branchCode: 'HKG', keyFinding: 'Optimal net margin spread across APAC operations.', actionItem: 'Maintain current interbank liquidity placement strategy.' },
|
||||||
|
{ branchCode: 'JPN', keyFinding: 'Ultra-low yield environment impacting asset returns.', actionItem: 'Shift $8M excess reserves into offshore USD notes.' },
|
||||||
|
{ branchCode: 'AFG', keyFinding: 'High cash-to-asset ratio for local risk provisioning.', actionItem: 'Keep operational liquidity in local central bank vault.' },
|
||||||
|
{ branchCode: 'EPZ', keyFinding: 'Export Processing Zone trade settlement expanding.', actionItem: 'Streamline multi-currency clearing with HKG.' },
|
||||||
|
{ branchCode: 'KOR', keyFinding: 'Branch closing down phase; orderly loan book runoff.', actionItem: 'Repatriate surplus capital back to Head Office reserve.' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenAIInstance() {
|
||||||
|
const key = process.env.OPENAI_API_KEY;
|
||||||
|
if (!key) return null;
|
||||||
|
return new OpenAI({ apiKey: key });
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI API Route for Balance Sheet Network Analysis
|
||||||
|
app.post('/api/ai/analyze-network', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { period, submissions } = req.body;
|
||||||
|
const client = getOpenAIInstance();
|
||||||
|
|
||||||
|
// Calculate aggregated network metrics
|
||||||
|
let totalAssets = 0;
|
||||||
|
let totalLiabilities = 0;
|
||||||
|
const branchSummaries: Record<string, { assets: number; liab: number; net: number }> = {};
|
||||||
|
|
||||||
|
BRANCHES_LIST.forEach((b) => {
|
||||||
|
const sub = submissions?.[b.id];
|
||||||
|
let bAssets = 0;
|
||||||
|
let bLiab = 0;
|
||||||
|
if (sub?.items) {
|
||||||
|
bAssets = ASSET_ITEMS_CONFIG.reduce((sum, cfg) => sum + (sub.items[cfg.key] || 0), 0);
|
||||||
|
bLiab = LIABILITY_ITEMS_CONFIG.reduce((sum, cfg) => sum + (sub.items[cfg.key] || 0), 0);
|
||||||
|
}
|
||||||
|
totalAssets += bAssets;
|
||||||
|
totalLiabilities += bLiab;
|
||||||
|
branchSummaries[b.code] = {
|
||||||
|
assets: Number(bAssets.toFixed(2)),
|
||||||
|
liab: Number(bLiab.toFixed(2)),
|
||||||
|
net: Number((bAssets - bLiab).toFixed(2)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const netEquity = totalAssets - totalLiabilities;
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
const fallbackAnalysis = generateRuleBasedAnalysis(
|
||||||
|
period || 'Current Period',
|
||||||
|
totalAssets,
|
||||||
|
totalLiabilities,
|
||||||
|
netEquity,
|
||||||
|
branchSummaries
|
||||||
|
);
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
isFallback: true,
|
||||||
|
message: 'OPENAI_API_KEY environment variable not configured. Operating in Quantum Fallback Engine mode.',
|
||||||
|
data: fallbackAnalysis,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt = `You are an elite Chief Risk Officer and Balance Sheet Optimization Specialist for a global multi-entity bank.
|
||||||
|
Analyze the provided network balance sheet summary matrix for reporting period ${period || 'Current'} and return actionable recommendations to optimize capital, liquidity, inter-branch yield spreads, and risk resilience.
|
||||||
|
|
||||||
|
IMPORTANT - ENTITY CODES:
|
||||||
|
The bank operates 9 specific network entities with these exact branch codes:
|
||||||
|
1. BHR (Bahrain)
|
||||||
|
2. BGD (Bangladesh)
|
||||||
|
3. KSA (Saudi Arabia)
|
||||||
|
4. DEU (Germany)
|
||||||
|
5. HKG (Hong Kong)
|
||||||
|
6. JPN (Japan)
|
||||||
|
7. AFG (Afghanistan)
|
||||||
|
8. EPZ (EPZ)
|
||||||
|
9. KOR (South Korea - closing down)
|
||||||
|
|
||||||
|
You MUST ONLY use these exact 3-letter branch codes (BHR, BGD, KSA, DEU, HKG, JPN, AFG, EPZ, KOR) in targetBranches and branchCode fields.
|
||||||
|
|
||||||
|
Respond STRICTLY with a valid JSON object matching this schema:
|
||||||
|
{
|
||||||
|
"executiveSummary": "concise narrative summary",
|
||||||
|
"networkHealthScore": 95,
|
||||||
|
"potentialYieldPickupBps": 38,
|
||||||
|
"estimatedAnnualSavingsUSD": "$4.5M",
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"id": "rec-01",
|
||||||
|
"category": "Inter-Branch Arbitrage" | "Liquidity & Duration" | "Capital Efficiency" | "Risk & Exposure",
|
||||||
|
"title": "short descriptive title",
|
||||||
|
"targetBranches": ["DEU", "HKG"],
|
||||||
|
"impact": "Critical" | "High" | "Medium",
|
||||||
|
"description": "concise actionable insight and strategy",
|
||||||
|
"actionSteps": ["step 1", "step 2"],
|
||||||
|
"estimatedValueAdd": "estimated savings or spread improvement"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"branchSpecificInsights": [
|
||||||
|
{
|
||||||
|
"branchCode": "BHR",
|
||||||
|
"keyFinding": "short finding",
|
||||||
|
"actionItem": "short action item"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const userPrompt = JSON.stringify({
|
||||||
|
reportingPeriod: period,
|
||||||
|
networkTotalsUSD_Mn: {
|
||||||
|
totalAssets: Number(totalAssets.toFixed(2)),
|
||||||
|
totalLiabilities: Number(totalLiabilities.toFixed(2)),
|
||||||
|
netEquity: Number(netEquity.toFixed(2)),
|
||||||
|
},
|
||||||
|
branchSummaries,
|
||||||
|
}, null, 2);
|
||||||
|
|
||||||
|
const completion = await client.chat.completions.create({
|
||||||
|
model: 'gpt-4o',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
temperature: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawContent = completion.choices[0]?.message?.content || '{}';
|
||||||
|
const parsedData = JSON.parse(rawContent);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
isFallback: false,
|
||||||
|
modelUsed: completion.model || 'gpt-4o',
|
||||||
|
data: parsedData,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('OpenAI Analysis Error:', error?.message || error);
|
||||||
|
const fallbackAnalysis = generateRuleBasedAnalysis(
|
||||||
|
req.body?.period || 'Current Period',
|
||||||
|
1250,
|
||||||
|
1100,
|
||||||
|
150,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
isFallback: true,
|
||||||
|
errorDetails: error?.message || 'OpenAI API request failed',
|
||||||
|
message: `OpenAI API request encountered an issue (${error?.message || 'Error'}). Operating in Quantum Fallback Engine mode.`,
|
||||||
|
data: fallbackAnalysis,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Reset database back to default seed data
|
// Reset database back to default seed data
|
||||||
app.post('/api/reset-seed', (req, res) => {
|
app.post('/api/reset-seed', (req, res) => {
|
||||||
if (fs.existsSync(dbPath)) {
|
if (fs.existsSync(dbPath)) {
|
||||||
|
|
|
||||||
41
src/App.tsx
41
src/App.tsx
|
|
@ -19,14 +19,48 @@ export default function App() {
|
||||||
const [activePeriod, setActivePeriod] = useState<string>('22-May-26');
|
const [activePeriod, setActivePeriod] = useState<string>('22-May-26');
|
||||||
const [priorPeriod, setPriorPeriod] = useState<string>('15-May-26');
|
const [priorPeriod, setPriorPeriod] = useState<string>('15-May-26');
|
||||||
const [periodsList, setPeriodsList] = useState<string[]>(['22-May-26', '15-May-26', '08-May-26', '01-May-26']);
|
const [periodsList, setPeriodsList] = useState<string[]>(['22-May-26', '15-May-26', '08-May-26', '01-May-26']);
|
||||||
|
<<<<<<< HEAD
|
||||||
const [themeMode, setThemeMode] = useState<HolographicThemeMode>('amber');
|
const [themeMode, setThemeMode] = useState<HolographicThemeMode>('amber');
|
||||||
|
=======
|
||||||
|
const [themeMode, setThemeMode] = useState<HolographicThemeMode>(() => (localStorage.getItem('app_theme_mode') as HolographicThemeMode) || 'amber');
|
||||||
|
>>>>>>> cea0aea (Commit v0.2)
|
||||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState<boolean>(false);
|
const [isAuthModalOpen, setIsAuthModalOpen] = useState<boolean>(false);
|
||||||
|
const [verificationNotice, setVerificationNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
const [submissions, setSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
|
const [submissions, setSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
|
||||||
const [priorSubmissions, setPriorSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
|
const [priorSubmissions, setPriorSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
|
||||||
const [allUsers, setAllUsers] = useState<User[]>(INITIAL_USERS);
|
const [allUsers, setAllUsers] = useState<User[]>(INITIAL_USERS);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// Sync theme to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('app_theme_mode', themeMode);
|
||||||
|
}, [themeMode]);
|
||||||
|
|
||||||
|
// Inspect URL parameters for email verification link trigger
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const verifyToken = urlParams.get('verifyToken');
|
||||||
|
const verifyEmail = urlParams.get('email');
|
||||||
|
|
||||||
|
if (verifyToken || verifyEmail) {
|
||||||
|
fetch('/api/auth/verify-email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token: verifyToken, email: verifyEmail }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.user) {
|
||||||
|
setVerificationNotice(data.message || 'Email address verified successfully! You can now log in.');
|
||||||
|
setIsAuthModalOpen(true);
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('Verification error:', err));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Auto switch tab if branch user logs in and is on admin-only view
|
// Auto switch tab if branch user logs in and is on admin-only view
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser.role === 'branch_user') {
|
if (currentUser.role === 'branch_user') {
|
||||||
|
|
@ -95,6 +129,8 @@ export default function App() {
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
allUsers={allUsers}
|
allUsers={allUsers}
|
||||||
|
themeMode={themeMode}
|
||||||
|
onThemeChange={setThemeMode}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -111,6 +147,8 @@ export default function App() {
|
||||||
setActivePeriod={setActivePeriod}
|
setActivePeriod={setActivePeriod}
|
||||||
periodsList={periodsList}
|
periodsList={periodsList}
|
||||||
allUsers={allUsers}
|
allUsers={allUsers}
|
||||||
|
themeMode={themeMode}
|
||||||
|
onThemeChange={setThemeMode}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -187,7 +225,7 @@ export default function App() {
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Floating Futuristic Status Footer */}
|
{/* Floating Futuristic Status Footer */}
|
||||||
<footer className="bg-slate-900/90 backdrop-blur-xl border-t border-cyan-500/30 py-2.5 px-6 text-[11px] text-slate-300 flex flex-wrap justify-between items-center font-mono shrink-0 relative z-20 shadow-[0_-4px_20px_rgba(0,0,0,0.4)]">
|
<footer className="bg-slate-950/95 backdrop-blur-2xl border-t border-cyan-500/50 py-2.5 px-6 text-[11px] text-slate-300 flex flex-wrap justify-between items-center font-mono shrink-0 relative z-20 shadow-[0_-10px_30px_rgba(0,0,0,0.85)]">
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<span className="text-cyan-400 font-bold flex items-center gap-1.5">
|
<span className="text-cyan-400 font-bold flex items-center gap-1.5">
|
||||||
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-ping"></span>
|
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-ping"></span>
|
||||||
|
|
@ -223,6 +261,7 @@ export default function App() {
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
setCurrentUser={setCurrentUser}
|
setCurrentUser={setCurrentUser}
|
||||||
allUsers={allUsers}
|
allUsers={allUsers}
|
||||||
|
verificationNotice={verificationNotice}
|
||||||
onLoginSuccess={loadPortalData}
|
onLoginSuccess={loadPortalData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
// New User Form State
|
// New User Form State
|
||||||
const [newEmail, setNewEmail] = useState('');
|
const [newEmail, setNewEmail] = useState('');
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [newBranchId, setNewBranchId] = useState<BranchId>('bahrain');
|
const [newBranchId, setNewBranchId] = useState<BranchId | 'head_office'>('head_office');
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -103,7 +103,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register new branch user
|
// Register new user (Branch or Head Office)
|
||||||
const handleRegisterUser = async (e: React.FormEvent) => {
|
const handleRegisterUser = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newEmail || !newName) return;
|
if (!newEmail || !newName) return;
|
||||||
|
|
@ -116,15 +116,16 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
email: newEmail,
|
email: newEmail,
|
||||||
name: newName,
|
name: newName,
|
||||||
branchId: newBranchId,
|
branchId: newBranchId,
|
||||||
role: 'branch_user',
|
role: newBranchId === 'head_office' ? 'admin' : 'branch_user',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setStatusMsg(`User ${newName} registered. Waiting for admin approval.`);
|
setStatusMsg(data.message || `User ${newName} registered. Verification link dispatched to ${newEmail}.`);
|
||||||
setNewEmail('');
|
setNewEmail('');
|
||||||
setNewName('');
|
setNewName('');
|
||||||
fetchData();
|
fetchData();
|
||||||
|
onUsersUpdated();
|
||||||
} else {
|
} else {
|
||||||
setStatusMsg(data.error || 'Registration failed.');
|
setStatusMsg(data.error || 'Registration failed.');
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +134,27 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resend Verification Email Link
|
||||||
|
const handleResendVerification = async (targetEmail: string) => {
|
||||||
|
setStatusMsg(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/resend-verification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: targetEmail }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
setStatusMsg(data.message || `Verification link re-sent to ${targetEmail}.`);
|
||||||
|
fetchData();
|
||||||
|
} else {
|
||||||
|
setStatusMsg(data.error || 'Failed to resend verification email.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Resend verification error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Force Reset Password for branch account
|
// Force Reset Password for branch account
|
||||||
const handleForceResetPassword = async (userId: string, targetName: string) => {
|
const handleForceResetPassword = async (userId: string, targetName: string) => {
|
||||||
setStatusMsg(null);
|
setStatusMsg(null);
|
||||||
|
|
@ -313,6 +335,16 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
Admin
|
Admin
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{u.isVerified ? (
|
||||||
|
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-1.5 rounded font-mono">
|
||||||
|
Verified
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-1.5 rounded animate-pulse font-mono flex items-center gap-1">
|
||||||
|
<Mail className="w-2.5 h-2.5 text-amber-400" />
|
||||||
|
Pending Verify
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{u.approved ? (
|
{u.approved ? (
|
||||||
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-1.5 rounded font-mono">
|
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-1.5 rounded font-mono">
|
||||||
Approved
|
Approved
|
||||||
|
|
@ -343,9 +375,18 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
<div className="text-[11px] text-slate-400 font-mono mt-0.5">{u.email} ({uBranch?.name || 'Head Office'})</div>
|
<div className="text-[11px] text-slate-400 font-mono mt-0.5">{u.email} ({uBranch?.name || 'Head Office'})</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{u.role !== 'admin' && (
|
|
||||||
<div className="flex items-center space-x-1 shrink-0 ml-2">
|
<div className="flex items-center space-x-1 shrink-0 ml-2">
|
||||||
{u.approved && (
|
{!u.isVerified && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleResendVerification(u.email)}
|
||||||
|
className="px-2 py-1 bg-blue-950/80 hover:bg-blue-900 border border-blue-500/40 text-blue-300 font-semibold rounded text-[10px] flex items-center gap-1 transition-colors cursor-pointer"
|
||||||
|
title="Resend email verification & registration process link"
|
||||||
|
>
|
||||||
|
<Mail className="w-3 h-3 text-blue-400" />
|
||||||
|
<span>Resend Link</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{u.role !== 'admin' && u.approved && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleForceResetPassword(u.id, u.name)}
|
onClick={() => handleForceResetPassword(u.id, u.name)}
|
||||||
className="px-2 py-1 bg-amber-950/60 hover:bg-amber-900 border border-amber-500/40 text-amber-300 font-semibold rounded text-[10px] flex items-center gap-1 transition-colors cursor-pointer"
|
className="px-2 py-1 bg-amber-950/60 hover:bg-amber-900 border border-amber-500/40 text-amber-300 font-semibold rounded text-[10px] flex items-center gap-1 transition-colors cursor-pointer"
|
||||||
|
|
@ -355,7 +396,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
<span>Force Reset</span>
|
<span>Force Reset</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!u.approved && (
|
{u.role !== 'admin' && !u.approved && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleApprove(u.id, true)}
|
onClick={() => handleApprove(u.id, true)}
|
||||||
className="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
|
className="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
|
||||||
|
|
@ -364,6 +405,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{u.role !== 'admin' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleApprove(u.id, false)}
|
onClick={() => handleApprove(u.id, false)}
|
||||||
className="px-2.5 py-1 bg-slate-800 hover:bg-rose-900 text-slate-300 hover:text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
|
className="px-2.5 py-1 bg-slate-800 hover:bg-rose-900 text-slate-300 hover:text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
|
||||||
|
|
@ -371,9 +413,9 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
<XCircle className="w-3 h-3" />
|
<XCircle className="w-3 h-3" />
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -407,9 +449,10 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
||||||
<div className="flex justify-between items-center text-xs">
|
<div className="flex justify-between items-center text-xs">
|
||||||
<select
|
<select
|
||||||
value={newBranchId}
|
value={newBranchId}
|
||||||
onChange={(e) => setNewBranchId(e.target.value as BranchId)}
|
onChange={(e) => setNewBranchId(e.target.value as BranchId | 'head_office')}
|
||||||
className="bg-slate-900 border border-slate-700 text-slate-200 rounded px-2.5 py-1.5 focus:outline-none"
|
className="bg-slate-900 border border-slate-700 text-slate-200 rounded px-2.5 py-1.5 focus:outline-none"
|
||||||
>
|
>
|
||||||
|
<option value="head_office">Head Office (HO)</option>
|
||||||
{BRANCHES_LIST.map((b) => (
|
{BRANCHES_LIST.map((b) => (
|
||||||
<option key={b.id} value={b.id}>
|
<option key={b.id} value={b.id}>
|
||||||
{b.name} ({b.code})
|
{b.name} ({b.code})
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ interface AuthModalProps {
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
setCurrentUser: (user: User) => void;
|
setCurrentUser: (user: User) => void;
|
||||||
allUsers: User[];
|
allUsers: User[];
|
||||||
|
verificationNotice?: string | null;
|
||||||
onLoginSuccess?: () => void;
|
onLoginSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
|
||||||
currentUser,
|
currentUser,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
verificationNotice,
|
||||||
onLoginSuccess
|
onLoginSuccess
|
||||||
}) => {
|
}) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
@ -248,6 +250,13 @@ export const AuthModal: React.FC<AuthModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feedback Banner */}
|
{/* Feedback Banner */}
|
||||||
|
{verificationNotice && (
|
||||||
|
<div className="mx-4 mt-3 p-3 bg-cyan-950/90 border border-cyan-400/60 rounded-xl text-cyan-200 text-xs flex items-center gap-2 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||||
|
<span>{verificationNotice}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loginError && !isForgotPassMode && (
|
{loginError && !isForgotPassMode && (
|
||||||
<div className="mx-4 mt-3 p-3 bg-rose-950/80 border border-rose-500/60 rounded-xl text-rose-200 text-xs flex items-center gap-2">
|
<div className="mx-4 mt-3 p-3 bg-rose-950/80 border border-rose-500/60 rounded-xl text-rose-200 text-xs flex items-center gap-2">
|
||||||
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
|
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0" />
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
BranchId
|
BranchId
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { InteractiveWorldMap } from './InteractiveWorldMap';
|
import { InteractiveWorldMap } from './InteractiveWorldMap';
|
||||||
|
import { OpenAINetworkOptimizer } from './OpenAINetworkOptimizer';
|
||||||
|
|
||||||
interface DashboardAnalyticsProps {
|
interface DashboardAnalyticsProps {
|
||||||
period: string;
|
period: string;
|
||||||
|
|
@ -163,6 +164,9 @@ export const DashboardAnalytics: React.FC<DashboardAnalyticsProps> = ({ period,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* OpenAI Quantum Network Balance Sheet Optimizer */}
|
||||||
|
<OpenAINetworkOptimizer period={period} submissions={submissions} />
|
||||||
|
|
||||||
{/* Interactive Global World Map View */}
|
{/* Interactive Global World Map View */}
|
||||||
<InteractiveWorldMap period={period} submissions={submissions} />
|
<InteractiveWorldMap period={period} submissions={submissions} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,13 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Key,
|
Key,
|
||||||
LogOut
|
LogOut,
|
||||||
|
Edit3,
|
||||||
|
Check,
|
||||||
|
Palette
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { User, BranchId, BRANCHES_LIST } from '../types';
|
import { User, BranchId, BRANCHES_LIST } from '../types';
|
||||||
|
import { HolographicThemeMode } from './HolographicCanvas';
|
||||||
|
|
||||||
interface NavigationProps {
|
interface NavigationProps {
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
|
|
@ -39,6 +43,8 @@ interface NavigationProps {
|
||||||
setActivePeriod: (period: string) => void;
|
setActivePeriod: (period: string) => void;
|
||||||
periodsList: string[];
|
periodsList: string[];
|
||||||
allUsers: User[];
|
allUsers: User[];
|
||||||
|
themeMode?: HolographicThemeMode;
|
||||||
|
onThemeChange?: (theme: HolographicThemeMode) => void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
onResetSeed?: () => void;
|
onResetSeed?: () => void;
|
||||||
onOpenAuthModal?: () => void;
|
onOpenAuthModal?: () => void;
|
||||||
|
|
@ -51,6 +57,8 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
||||||
activeTab,
|
activeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
themeMode,
|
||||||
|
onThemeChange,
|
||||||
onLogout,
|
onLogout,
|
||||||
onOpenAuthModal
|
onOpenAuthModal
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -59,11 +67,23 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
||||||
const isAdmin = currentUser.role === 'admin';
|
const isAdmin = currentUser.role === 'admin';
|
||||||
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
|
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
|
||||||
|
|
||||||
|
// Editable Equity Core system title
|
||||||
|
const [brandName, setBrandName] = useState(() => localStorage.getItem('app_brand_title') || 'EQUITY CORE');
|
||||||
|
const [isEditingBrand, setIsEditingBrand] = useState(false);
|
||||||
|
const [editBrandValue, setEditBrandValue] = useState(brandName);
|
||||||
|
|
||||||
|
const saveBrandName = () => {
|
||||||
|
const trimmed = editBrandValue.trim() || 'EQUITY CORE';
|
||||||
|
setBrandName(trimmed);
|
||||||
|
localStorage.setItem('app_brand_title', trimmed);
|
||||||
|
setIsEditingBrand(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={`transition-all duration-300 ease-in-out ${
|
className={`transition-all duration-300 ease-in-out ${
|
||||||
isCollapsed ? 'w-16' : 'w-60 lg:w-64'
|
isCollapsed ? 'w-16' : 'w-60 lg:w-64'
|
||||||
} bg-slate-900/95 backdrop-blur-2xl border-r border-cyan-500/40 flex flex-col shrink-0 select-none z-30 hidden md:flex hud-corner shadow-[4px_0_25px_rgba(0,0,0,0.6)] h-full overflow-x-hidden`}
|
} nav-standout border-r border-cyan-500/50 flex flex-col shrink-0 select-none z-30 hidden md:flex hud-corner shadow-[8px_0_35px_rgba(0,0,0,0.85)] h-full overflow-x-hidden`}
|
||||||
>
|
>
|
||||||
{/* Brand Header with Holographic Glow */}
|
{/* Brand Header with Holographic Glow */}
|
||||||
<div className="p-3.5 border-b border-cyan-500/20 flex items-center justify-between bg-cyan-950/20 shrink-0">
|
<div className="p-3.5 border-b border-cyan-500/20 flex items-center justify-between bg-cyan-950/20 shrink-0">
|
||||||
|
|
@ -73,9 +93,36 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
||||||
B
|
B
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden whitespace-nowrap">
|
<div className="overflow-hidden whitespace-nowrap">
|
||||||
<h1 className="text-sm font-bold tracking-tight text-white flex items-center gap-0.5">
|
{isEditingBrand ? (
|
||||||
EQUITY<span className="text-cyan-400 font-extrabold glow-text-cyan">CORE</span>
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editBrandValue}
|
||||||
|
onChange={(e) => setEditBrandValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter') saveBrandName(); }}
|
||||||
|
className="bg-slate-950 text-cyan-300 font-extrabold text-xs border border-cyan-400 rounded px-1.5 py-0.5 focus:outline-none w-28 font-mono"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={saveBrandName}
|
||||||
|
className="p-1 text-emerald-400 hover:text-emerald-300 cursor-pointer"
|
||||||
|
title="Save System Name"
|
||||||
|
>
|
||||||
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={() => setIsEditingBrand(true)}
|
||||||
|
className="group flex items-center gap-1 cursor-pointer"
|
||||||
|
title="Click to edit system title"
|
||||||
|
>
|
||||||
|
<h1 className="text-xs sm:text-sm font-bold tracking-tight text-white flex items-center gap-0.5 uppercase">
|
||||||
|
{brandName}
|
||||||
</h1>
|
</h1>
|
||||||
|
<Edit3 className="w-3 h-3 text-cyan-400/60 group-hover:text-cyan-300 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className="text-[9px] text-cyan-300/70 font-mono tracking-widest uppercase">QUANTUM BALANCE SHEET</p>
|
<p className="text-[9px] text-cyan-300/70 font-mono tracking-widest uppercase">QUANTUM BALANCE SHEET</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -367,6 +414,8 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
||||||
setActivePeriod,
|
setActivePeriod,
|
||||||
periodsList,
|
periodsList,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
themeMode = 'amber',
|
||||||
|
onThemeChange,
|
||||||
onLogout,
|
onLogout,
|
||||||
onResetSeed,
|
onResetSeed,
|
||||||
onOpenAuthModal
|
onOpenAuthModal
|
||||||
|
|
@ -375,6 +424,8 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
||||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||||
const [countdown, setCountdown] = useState({ days: 3, hours: 14, mins: 22 });
|
const [countdown, setCountdown] = useState({ days: 3, hours: 14, mins: 22 });
|
||||||
|
|
||||||
|
const brandName = localStorage.getItem('app_brand_title') || 'EQUITY CORE';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setCountdown((prev) => {
|
setCountdown((prev) => {
|
||||||
|
|
@ -389,7 +440,7 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
||||||
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
|
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-14 border-b border-cyan-500/40 bg-slate-900/95 backdrop-blur-2xl flex items-center justify-between px-4 sm:px-6 shrink-0 z-20 w-full shadow-[0_4px_20px_rgba(0,0,0,0.5)] relative">
|
<header className="h-14 topheader-standout border-b border-cyan-500/50 flex items-center justify-between px-4 sm:px-6 shrink-0 z-20 w-full shadow-[0_10px_35px_rgba(0,0,0,0.85)] relative">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
{/* Mobile Menu Toggle Button */}
|
{/* Mobile Menu Toggle Button */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -403,7 +454,7 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
||||||
{/* Brand Badge */}
|
{/* Brand Badge */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-7 h-7 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 text-xs shadow-[0_0_10px_#06b6d4]">B</div>
|
<div className="w-7 h-7 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 text-xs shadow-[0_0_10px_#06b6d4]">B</div>
|
||||||
<span className="font-bold text-white text-xs tracking-wider md:hidden">EQUITY<span className="text-cyan-400">CORE</span></span>
|
<span className="font-bold text-white text-xs tracking-wider md:hidden uppercase">{brandName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-xs sm:text-sm font-bold text-white hidden sm:flex items-center gap-2 glow-text-cyan">
|
<h2 className="text-xs sm:text-sm font-bold text-white hidden sm:flex items-center gap-2 glow-text-cyan">
|
||||||
|
|
@ -437,6 +488,42 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Theme Mode Switcher (Pinnacle Theme Selector) */}
|
||||||
|
<div className="flex items-center gap-1.5 bg-slate-950/80 px-2.5 py-1 rounded-xl border border-cyan-500/30 shadow-inner">
|
||||||
|
<Palette className="w-3.5 h-3.5 text-cyan-400 hidden sm:inline-block" />
|
||||||
|
<span className="text-[10px] font-mono font-bold text-slate-400 hidden md:inline-block uppercase">Theme:</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onThemeChange?.('amber')}
|
||||||
|
className={`w-3.5 h-3.5 rounded-full transition-all cursor-pointer ${
|
||||||
|
themeMode === 'amber' ? 'bg-amber-400 ring-2 ring-white scale-110 shadow-[0_0_10px_#f59e0b]' : 'bg-amber-950 border border-amber-800 opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
title="Amber Tactical HUD Theme"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => onThemeChange?.('cyan')}
|
||||||
|
className={`w-3.5 h-3.5 rounded-full transition-all cursor-pointer ${
|
||||||
|
themeMode === 'cyan' ? 'bg-cyan-400 ring-2 ring-white scale-110 shadow-[0_0_10px_#06b6d4]' : 'bg-cyan-950 border border-cyan-800 opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
title="Quantum Cyan Theme"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => onThemeChange?.('synthwave')}
|
||||||
|
className={`w-3.5 h-3.5 rounded-full transition-all cursor-pointer ${
|
||||||
|
themeMode === 'synthwave' ? 'bg-pink-500 ring-2 ring-white scale-110 shadow-[0_0_10px_#ec4899]' : 'bg-pink-950 border border-pink-800 opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
title="Synthwave Neon Theme"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => onThemeChange?.('matrix')}
|
||||||
|
className={`w-3.5 h-3.5 rounded-full transition-all cursor-pointer ${
|
||||||
|
themeMode === 'matrix' ? 'bg-emerald-400 ring-2 ring-white scale-110 shadow-[0_0_10px_#10b981]' : 'bg-emerald-950 border border-emerald-800 opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
title="Deep Matrix Emerald Theme"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="hidden lg:flex items-center gap-2 bg-slate-900/80 px-3 py-1 rounded-xl border border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.1)]">
|
<div className="hidden lg:flex items-center gap-2 bg-slate-900/80 px-3 py-1 rounded-xl border border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.1)]">
|
||||||
<Clock className="w-3.5 h-3.5 text-amber-400 animate-pulse" />
|
<Clock className="w-3.5 h-3.5 text-amber-400 animate-pulse" />
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|
|
||||||
429
src/components/OpenAINetworkOptimizer.tsx
Normal file
429
src/components/OpenAINetworkOptimizer.tsx
Normal file
|
|
@ -0,0 +1,429 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
BrainCircuit,
|
||||||
|
ShieldAlert,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
RefreshCw,
|
||||||
|
Zap,
|
||||||
|
DollarSign,
|
||||||
|
Layers,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Building,
|
||||||
|
Info
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { BranchId, BranchSubmission, BRANCHES_LIST } from '../types';
|
||||||
|
|
||||||
|
interface OpenAINetworkOptimizerProps {
|
||||||
|
period: string;
|
||||||
|
submissions: Record<BranchId, BranchSubmission>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecommendationItem {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
title: string;
|
||||||
|
targetBranches: string[];
|
||||||
|
impact: 'Critical' | 'High' | 'Medium';
|
||||||
|
description: string;
|
||||||
|
actionSteps: string[];
|
||||||
|
estimatedValueAdd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BranchInsight {
|
||||||
|
branchCode: string;
|
||||||
|
keyFinding: string;
|
||||||
|
actionItem: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AIAnalysisData {
|
||||||
|
executiveSummary: string;
|
||||||
|
networkHealthScore: number;
|
||||||
|
potentialYieldPickupBps: number;
|
||||||
|
estimatedAnnualSavingsUSD: string;
|
||||||
|
recommendations: RecommendationItem[];
|
||||||
|
branchSpecificInsights: BranchInsight[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenAINetworkOptimizer: React.FC<OpenAINetworkOptimizerProps> = ({ period, submissions }) => {
|
||||||
|
const [analysisData, setAnalysisData] = useState<AIAnalysisData | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [isFallback, setIsFallback] = useState<boolean>(false);
|
||||||
|
const [modelUsed, setModelUsed] = useState<string>('gpt-4o');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// UI States
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('ALL');
|
||||||
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
|
const [expandedRecId, setExpandedRecId] = useState<string | null>(null);
|
||||||
|
const [completedSteps, setCompletedSteps] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const fetchAIAnalysis = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ai/analyze-network', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
period,
|
||||||
|
submissions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resData = await response.json();
|
||||||
|
if (resData.data) {
|
||||||
|
setAnalysisData(resData.data);
|
||||||
|
setIsFallback(!!resData.isFallback);
|
||||||
|
setModelUsed(resData.modelUsed || 'gpt-4o');
|
||||||
|
if (resData.data.recommendations && resData.data.recommendations.length > 0) {
|
||||||
|
setExpandedRecId(resData.data.recommendations[0].id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(resData.errorDetails || 'Failed to parse AI response');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to analyze network matrix:', err);
|
||||||
|
setErrorMessage(err.message || 'Network analysis failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAIAnalysis();
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
const copyRecommendation = (rec: RecommendationItem) => {
|
||||||
|
const text = `[OPENAI QUANTUM RECOMMENDATION - ${rec.title}]\nCategory: ${rec.category}\nImpact: ${rec.impact}\nTarget Branches: ${rec.targetBranches.join(', ')}\nEstimated Value Add: ${rec.estimatedValueAdd}\n\nDescription:\n${rec.description}\n\nAction Steps:\n${rec.actionSteps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`;
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopiedId(rec.id);
|
||||||
|
setTimeout(() => setCopiedId(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleStep = (stepKey: string) => {
|
||||||
|
setCompletedSteps((prev) => ({ ...prev, [stepKey]: !prev[stepKey] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories = ['ALL', 'Inter-Branch Arbitrage', 'Capital Efficiency', 'Liquidity & Duration', 'Risk & Exposure'];
|
||||||
|
|
||||||
|
const filteredRecs = analysisData?.recommendations.filter((rec) => {
|
||||||
|
if (selectedCategory === 'ALL') return true;
|
||||||
|
return rec.category.toLowerCase().includes(selectedCategory.toLowerCase());
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="holo-card rounded-2xl p-6 border border-cyan-500/40 shadow-[0_10px_35px_rgba(0,0,0,0.8)] space-y-6 relative overflow-hidden bg-slate-950/90">
|
||||||
|
{/* Background Decorative Gradient Mesh */}
|
||||||
|
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-br from-cyan-500/10 via-purple-500/5 to-transparent rounded-full blur-3xl pointer-events-none -mr-20 -mt-20"></div>
|
||||||
|
|
||||||
|
{/* Header Bar */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-cyan-500/30 pb-4 relative z-10">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-cyan-600 via-indigo-600 to-purple-600 p-0.5 shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||||
|
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center text-cyan-400">
|
||||||
|
<BrainCircuit className="w-5 h-5 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-base font-extrabold text-white tracking-wide uppercase flex items-center gap-2">
|
||||||
|
OpenAI Balance Sheet Network Matrix Analyzer
|
||||||
|
</h3>
|
||||||
|
<span className={`text-[10px] font-mono px-2 py-0.5 rounded-full border ${
|
||||||
|
isFallback
|
||||||
|
? 'bg-amber-950/80 text-amber-300 border-amber-500/40'
|
||||||
|
: 'bg-emerald-950/80 text-emerald-300 border-emerald-500/40'
|
||||||
|
}`}>
|
||||||
|
{isFallback ? '⚡ Quantum Algorithmic Engine' : `✨ OpenAI ${modelUsed}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
Automated AI recommendations to optimize inter-branch capital, liquidity gaps, and net yield spreads.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => fetchAIAnalysis()}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-1.5 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-slate-950 font-bold text-xs font-mono transition-all flex items-center gap-2 cursor-pointer shadow-[0_0_20px_rgba(6,182,212,0.4)] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
<span>{loading ? 'Analyzing Matrix...' : 'Re-Run Matrix Analysis'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="bg-rose-950/80 border border-rose-500/50 rounded-xl p-3 text-xs text-rose-200 flex items-center gap-2">
|
||||||
|
<Info className="w-4 h-4 text-rose-400 shrink-0" />
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading Skeleton Matrix Animation */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="py-12 flex flex-col items-center justify-center space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-16 h-16 rounded-full border-4 border-cyan-500/20 border-t-cyan-400 animate-spin"></div>
|
||||||
|
<Sparkles className="w-6 h-6 text-cyan-400 absolute inset-0 m-auto animate-ping" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<p className="text-sm font-bold text-cyan-300 font-mono tracking-wider">
|
||||||
|
QUANTUM AI MATRIX SCAN IN PROGRESS...
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-400 font-mono">
|
||||||
|
Ingesting 9 branch balance sheet submissions & Calculating Inter-Branch Spread Opportunities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : analysisData ? (
|
||||||
|
<div className="space-y-6 relative z-10">
|
||||||
|
{/* Executive Summary Banner */}
|
||||||
|
<div className="bg-gradient-to-r from-cyan-950/80 via-slate-900 to-indigo-950/80 border border-cyan-500/40 rounded-xl p-4 shadow-lg space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs font-bold text-cyan-400 uppercase tracking-widest">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Sparkles className="w-4 h-4 text-cyan-300" />
|
||||||
|
Executive Posture Narrative
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono">Period: {period}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-200 leading-relaxed font-sans">
|
||||||
|
{analysisData.executiveSummary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Scorecard Metrics */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-slate-900/90 border border-emerald-500/40 rounded-xl p-4 shadow-md space-y-1 relative overflow-hidden">
|
||||||
|
<div className="flex justify-between items-center text-slate-400 text-[10px] font-bold uppercase tracking-wider">
|
||||||
|
<span>Network Health Score</span>
|
||||||
|
<ShieldAlert className="w-4 h-4 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-emerald-400 font-mono">
|
||||||
|
{analysisData.networkHealthScore} / 100
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-emerald-400 h-full rounded-full transition-all duration-1000"
|
||||||
|
style={{ width: `${analysisData.networkHealthScore}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-emerald-300/80 font-mono pt-0.5">Optimal Capital Resilience</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900/90 border border-cyan-500/40 rounded-xl p-4 shadow-md space-y-1 relative overflow-hidden">
|
||||||
|
<div className="flex justify-between items-center text-slate-400 text-[10px] font-bold uppercase tracking-wider">
|
||||||
|
<span>Potential Spread Pickup</span>
|
||||||
|
<Zap className="w-4 h-4 text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-cyan-300 font-mono">
|
||||||
|
+{analysisData.potentialYieldPickupBps} bps
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-cyan-400/80 font-mono">Inter-Branch Arbitrage Opportunity</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900/90 border border-amber-500/40 rounded-xl p-4 shadow-md space-y-1 relative overflow-hidden">
|
||||||
|
<div className="flex justify-between items-center text-slate-400 text-[10px] font-bold uppercase tracking-wider">
|
||||||
|
<span>Est. Annual Value Add</span>
|
||||||
|
<DollarSign className="w-4 h-4 text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-amber-300 font-mono">
|
||||||
|
{analysisData.estimatedAnnualSavingsUSD}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-amber-400/80 font-mono">Optimized Funding & Yield</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900/90 border border-purple-500/40 rounded-xl p-4 shadow-md space-y-1 relative overflow-hidden">
|
||||||
|
<div className="flex justify-between items-center text-slate-400 text-[10px] font-bold uppercase tracking-wider">
|
||||||
|
<span>Actionable Strategies</span>
|
||||||
|
<Layers className="w-4 h-4 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-purple-300 font-mono">
|
||||||
|
0{analysisData.recommendations?.length || 0} Identified
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-purple-300/80 font-mono">Tailored for HO & Subsidiaries</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter Pills */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 pt-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-[10px] font-mono text-slate-400 uppercase tracking-widest mr-1">Filter Strategy:</span>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => setSelectedCategory(cat)}
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-mono font-bold transition-all cursor-pointer ${
|
||||||
|
selectedCategory === cat
|
||||||
|
? 'bg-cyan-500 text-slate-950 shadow-[0_0_12px_rgba(6,182,212,0.4)]'
|
||||||
|
: 'bg-slate-900 text-slate-400 hover:text-white border border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recommendations List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredRecs.length === 0 ? (
|
||||||
|
<div className="p-8 text-center bg-slate-900/50 rounded-xl border border-slate-800 text-xs text-slate-400 font-mono">
|
||||||
|
No recommendations match the selected category filter.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredRecs.map((rec) => {
|
||||||
|
const isExpanded = expandedRecId === rec.id;
|
||||||
|
const isCopied = copiedId === rec.id;
|
||||||
|
|
||||||
|
const impactColors = {
|
||||||
|
Critical: 'bg-rose-950/80 text-rose-300 border-rose-500/50',
|
||||||
|
High: 'bg-amber-950/80 text-amber-300 border-amber-500/50',
|
||||||
|
Medium: 'bg-blue-950/80 text-blue-300 border-blue-500/50',
|
||||||
|
}[rec.impact];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={rec.id}
|
||||||
|
className="bg-slate-900/90 border border-slate-800 hover:border-cyan-500/50 rounded-xl p-4 shadow-lg transition-all space-y-3 relative group"
|
||||||
|
>
|
||||||
|
{/* Rec Header */}
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div className="space-y-1.5 flex-1">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className={`text-[10px] font-mono px-2 py-0.5 rounded border uppercase font-bold ${impactColors}`}>
|
||||||
|
{rec.impact} Priority
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono text-cyan-400 bg-cyan-950/80 border border-cyan-500/30 px-2 py-0.5 rounded">
|
||||||
|
{rec.category}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{rec.targetBranches.map((b) => {
|
||||||
|
const bInfo = BRANCHES_LIST.find((br) => br.code.toUpperCase() === b.toUpperCase() || br.id.toLowerCase() === b.toLowerCase());
|
||||||
|
return (
|
||||||
|
<span key={b} className="text-[10px] font-mono bg-slate-950 text-cyan-300 border border-cyan-500/30 px-1.5 py-0.5 rounded" title={bInfo?.name}>
|
||||||
|
{bInfo ? `${bInfo.code}` : b}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="text-sm font-bold text-white group-hover:text-cyan-300 transition-colors flex items-center gap-2">
|
||||||
|
{rec.title}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono font-bold text-amber-400 bg-amber-950/50 border border-amber-500/30 px-2.5 py-1 rounded-lg">
|
||||||
|
{rec.estimatedValueAdd}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => copyRecommendation(rec)}
|
||||||
|
className="p-1.5 rounded-lg bg-slate-950 border border-slate-800 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
||||||
|
title="Copy Recommendation Details"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedRecId(isExpanded ? null : rec.id)}
|
||||||
|
className="p-1.5 rounded-lg bg-slate-950 border border-slate-800 text-slate-400 hover:text-white transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed font-sans">
|
||||||
|
{rec.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Expanded Action Steps Checklist */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="pt-3 border-t border-slate-800 space-y-3 animate-fadeIn">
|
||||||
|
<span className="text-[10px] font-mono text-cyan-400 uppercase tracking-widest block font-bold">
|
||||||
|
Execution Checklist & ALCO Directives:
|
||||||
|
</span>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{rec.actionSteps.map((step, idx) => {
|
||||||
|
const stepKey = `${rec.id}-step-${idx}`;
|
||||||
|
const isDone = !!completedSteps[stepKey];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
onClick={() => toggleStep(stepKey)}
|
||||||
|
className={`flex items-start gap-2.5 p-2 rounded-lg border text-xs cursor-pointer transition-all ${
|
||||||
|
isDone
|
||||||
|
? 'bg-emerald-950/40 border-emerald-500/40 text-emerald-300 line-through'
|
||||||
|
: 'bg-slate-950/60 border-slate-800 hover:border-cyan-500/30 text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-4 h-4 rounded mt-0.5 flex items-center justify-center shrink-0 border ${
|
||||||
|
isDone ? 'bg-emerald-500 border-emerald-400 text-slate-950' : 'border-slate-600'
|
||||||
|
}`}>
|
||||||
|
{isDone && <Check className="w-3 h-3 stroke-[3]" />}
|
||||||
|
</div>
|
||||||
|
<span>{step}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Branch Specific Intelligence Grid */}
|
||||||
|
{analysisData.branchSpecificInsights && analysisData.branchSpecificInsights.length > 0 && (
|
||||||
|
<div className="pt-4 border-t border-slate-800 space-y-3">
|
||||||
|
<h4 className="text-xs font-bold text-white uppercase tracking-wider font-mono flex items-center gap-2">
|
||||||
|
<Building className="w-4 h-4 text-indigo-400" />
|
||||||
|
Branch-by-Branch Micro Strategy Matrix
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{analysisData.branchSpecificInsights.map((insight) => {
|
||||||
|
const branchInfo = BRANCHES_LIST.find((b) => b.code.toUpperCase() === insight.branchCode.toUpperCase() || b.id.toLowerCase() === insight.branchCode.toLowerCase());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={insight.branchCode}
|
||||||
|
className="bg-slate-900/80 border border-slate-800 p-3 rounded-xl space-y-1 text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center border-b border-slate-800/80 pb-1.5">
|
||||||
|
<span className="font-bold text-cyan-300 font-mono flex items-center gap-1.5">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-cyan-400"></span>
|
||||||
|
{branchInfo ? branchInfo.name : insight.branchCode} ({branchInfo ? branchInfo.code : insight.branchCode})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-300 pt-1">
|
||||||
|
<strong className="text-slate-400 font-normal">Finding: </strong>
|
||||||
|
{insight.keyFinding}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-emerald-400 pt-0.5 font-mono">
|
||||||
|
➔ {insight.actionItem}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
src/index.css
112
src/index.css
|
|
@ -1,43 +1,111 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Holographic Glassmorphism */
|
/* Holographic Glassmorphism & Elevated Card Standouts */
|
||||||
.holo-card {
|
.holo-card {
|
||||||
background: rgba(15, 23, 42, 0.94);
|
background: linear-gradient(145deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.95) 100%);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(24px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
border: 1px solid rgba(6, 182, 212, 0.45);
|
border: 1px solid rgba(6, 182, 212, 0.55);
|
||||||
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
|
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
|
||||||
inset 0 0 16px 0 rgba(6, 182, 212, 0.15);
|
0 0 1px 1px rgba(6, 182, 212, 0.3),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.15),
|
||||||
|
inset 0 0 20px 0 rgba(6, 182, 212, 0.12);
|
||||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.holo-card:hover {
|
.holo-card:hover {
|
||||||
border-color: rgba(6, 182, 212, 0.75);
|
border-color: rgba(6, 182, 212, 0.85);
|
||||||
box-shadow: 0 14px 45px 0 rgba(6, 182, 212, 0.3),
|
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
|
||||||
inset 0 0 24px 0 rgba(6, 182, 212, 0.25);
|
0 0 20px 0 rgba(6, 182, 212, 0.45),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0 0 28px 0 rgba(6, 182, 212, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.holo-card-magenta {
|
.holo-card-magenta {
|
||||||
background: rgba(15, 23, 42, 0.94);
|
background: linear-gradient(145deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.95) 100%);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(24px);
|
||||||
border: 1px solid rgba(236, 72, 153, 0.45);
|
border: 1px solid rgba(236, 72, 153, 0.55);
|
||||||
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
|
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
|
||||||
inset 0 0 16px 0 rgba(236, 72, 153, 0.15);
|
0 0 1px 1px rgba(236, 72, 153, 0.3),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.15),
|
||||||
|
inset 0 0 20px 0 rgba(236, 72, 153, 0.12);
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.holo-card-magenta:hover {
|
.holo-card-magenta:hover {
|
||||||
border-color: rgba(236, 72, 153, 0.75);
|
border-color: rgba(236, 72, 153, 0.85);
|
||||||
box-shadow: 0 14px 45px 0 rgba(236, 72, 153, 0.3),
|
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
|
||||||
inset 0 0 24px 0 rgba(236, 72, 153, 0.25);
|
0 0 20px 0 rgba(236, 72, 153, 0.45),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0 0 28px 0 rgba(236, 72, 153, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.holo-card-amber {
|
.holo-card-amber {
|
||||||
background: rgba(15, 23, 42, 0.94);
|
background: linear-gradient(145deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.95) 100%);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.55);
|
||||||
|
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
|
||||||
|
0 0 1px 1px rgba(245, 158, 11, 0.3),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.15),
|
||||||
|
inset 0 0 20px 0 rgba(245, 158, 11, 0.12);
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-card-amber:hover {
|
||||||
|
border-color: rgba(245, 158, 11, 0.85);
|
||||||
|
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 20px 0 rgba(245, 158, 11, 0.45),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0 0 28px 0 rgba(245, 158, 11, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-card-emerald {
|
||||||
|
background: linear-gradient(145deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.95) 100%);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.55);
|
||||||
|
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
|
||||||
|
0 0 1px 1px rgba(16, 185, 129, 0.3),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.15),
|
||||||
|
inset 0 0 20px 0 rgba(16, 185, 129, 0.12);
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-card-emerald:hover {
|
||||||
|
border-color: rgba(16, 185, 129, 0.85);
|
||||||
|
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 20px 0 rgba(16, 185, 129, 0.45),
|
||||||
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0 0 28px 0 rgba(16, 185, 129, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Universal Container Elevation & Standout Panel Styles */
|
||||||
|
.standout-panel {
|
||||||
|
background: linear-gradient(145deg, rgba(15, 23, 42, 0.96) 0%, rgba(2, 6, 23, 0.98) 100%);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
border: 1px solid rgba(245, 158, 11, 0.45);
|
border: 1px solid rgba(51, 65, 85, 0.8);
|
||||||
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.75),
|
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.8),
|
||||||
inset 0 0 16px 0 rgba(245, 158, 11, 0.15);
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-standout {
|
||||||
|
background: linear-gradient(180deg, rgba(15, 23, 42, 0.98) 0%, rgba(2, 6, 23, 0.99) 100%);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
border-right: 1px solid rgba(6, 182, 212, 0.45);
|
||||||
|
box-shadow: 10px 0 35px 0 rgba(0, 0, 0, 0.85),
|
||||||
|
inset -1px 0 0 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topheader-standout {
|
||||||
|
background: linear-gradient(90deg, rgba(15, 23, 42, 0.98) 0%, rgba(2, 6, 23, 0.99) 100%);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
border-bottom: 1px solid rgba(6, 182, 212, 0.45);
|
||||||
|
box-shadow: 0 10px 35px 0 rgba(0, 0, 0, 0.85),
|
||||||
|
inset 0 -1px 0 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neon Glow Text */
|
/* Neon Glow Text */
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,15 @@ export interface User {
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
branchId?: BranchId;
|
branchId?: BranchId | 'head_office';
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
mustChangePassword?: boolean;
|
mustChangePassword?: boolean;
|
||||||
isPasswordChanged?: boolean;
|
isPasswordChanged?: boolean;
|
||||||
|
isVerified?: boolean;
|
||||||
|
verificationToken?: string;
|
||||||
|
verifiedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlacementBorrowingType = 'Inter Branch' | 'Inter Bank';
|
export type PlacementBorrowingType = 'Inter Branch' | 'Inter Bank';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue