Commit v0.2

This commit is contained in:
Huzaifa Inam 2026-08-10 10:14:28 +05:00
parent 6339b8a43b
commit 614dc7e256
14 changed files with 1096 additions and 74 deletions

View file

@ -16,5 +16,5 @@ DATA_PATH=/app/data/portal-data.json
# SMTP_PASS=your_secret_smtp_password
# SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
# SMTP_USE_TLS=true
# SMTP_AUTO_REMINDERS_ENABLED=true
# SMTP_REMINDER_FREQUENCY_DAYS=7
# OpenAI API Key for Quantum Balance Sheet Network AI Recommendations
OPENAI_API_KEY=

View file

@ -15,6 +15,7 @@
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"nodemailer": "^9.0.4",
"openai": "^7.4.0",
"pg": "^8.22.0",
"react": "^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=="],
"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=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
<title>International Network Matrix</title>
</head>
<body>
<div id="root"></div>

View file

@ -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.",
"requestFramePermissions": [],
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]

View file

@ -22,6 +22,7 @@
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"nodemailer": "^9.0.4",
"openai": "^7.4.0",
"pg": "^8.22.0",
"react": "^19.0.1",
"react-dom": "^19.0.1",

358
server.ts
View file

@ -3,6 +3,7 @@ import path from 'path';
import fs from 'fs';
import { createServer as createViteServer } from 'vite';
import nodemailer from 'nodemailer';
import OpenAI from 'openai';
import {
INITIAL_USERS,
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 {
BRANCHES_LIST,
ASSET_ITEMS_CONFIG,
LIABILITY_ITEMS_CONFIG,
User,
Role,
BranchSubmission,
SmtpConfig,
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 });
});
// Register new user (requires Admin Approval)
app.post('/api/auth/register', (req, res) => {
// Register new user (requires Email Verification & Admin Approval)
app.post('/api/auth/register', async (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());
const cleanEmail = email.trim().toLowerCase();
const existing = db.users.find((u) => u.email.toLowerCase() === cleanEmail);
if (existing) {
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 = {
id: `user-${Date.now()}`,
email: email.trim().toLowerCase(),
email: cleanEmail,
name: name.trim(),
role: role || 'branch_user',
branchId: branchId || undefined,
approved: false, // Must be approved by Admin
role: assignedRole,
branchId: branchId || (assignedRole === 'admin' ? 'head_office' : 'bahrain'),
approved: assignedRole === 'admin', // Head office pre-approved
isVerified: false,
verificationToken,
createdAt: new Date().toISOString(),
};
db.users.push(newUser);
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({
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',
subject: `New User Registration Pending: ${newUser.name}`,
body: `User ${newUser.name} (${newUser.email}) requested access for branch ${newUser.branchId || 'N/A'}. Approval required.`,
subject: `New User Registration & Verification Dispatched: ${newUser.name}`,
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(),
status: 'simulated',
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)
@ -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
app.post('/api/reset-seed', (req, res) => {
if (fs.existsSync(dbPath)) {

View file

@ -19,14 +19,48 @@ export default function App() {
const [activePeriod, setActivePeriod] = useState<string>('22-May-26');
const [priorPeriod, setPriorPeriod] = useState<string>('15-May-26');
const [periodsList, setPeriodsList] = useState<string[]>(['22-May-26', '15-May-26', '08-May-26', '01-May-26']);
<<<<<<< HEAD
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 [verificationNotice, setVerificationNotice] = useState<string | null>(null);
const [submissions, setSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
const [priorSubmissions, setPriorSubmissions] = useState<Record<BranchId, BranchSubmission>>({} as any);
const [allUsers, setAllUsers] = useState<User[]>(INITIAL_USERS);
const [loading, setLoading] = useState<boolean>(true);
// 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
useEffect(() => {
if (currentUser.role === 'branch_user') {
@ -95,6 +129,8 @@ export default function App() {
activeTab={activeTab}
setActiveTab={setActiveTab}
allUsers={allUsers}
themeMode={themeMode}
onThemeChange={setThemeMode}
onLogout={handleLogout}
onOpenAuthModal={() => setIsAuthModalOpen(true)}
/>
@ -111,6 +147,8 @@ export default function App() {
setActivePeriod={setActivePeriod}
periodsList={periodsList}
allUsers={allUsers}
themeMode={themeMode}
onThemeChange={setThemeMode}
onLogout={handleLogout}
onOpenAuthModal={() => setIsAuthModalOpen(true)}
/>
@ -187,7 +225,7 @@ export default function App() {
</main>
{/* Floating Futuristic Status Footer */}
<footer className="bg-slate-900/90 backdrop-blur-xl border-t border-cyan-500/30 py-2.5 px-6 text-[11px] text-slate-300 flex flex-wrap justify-between items-center font-mono shrink-0 relative z-20 shadow-[0_-4px_20px_rgba(0,0,0,0.4)]">
<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">
<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>
@ -223,6 +261,7 @@ export default function App() {
currentUser={currentUser}
setCurrentUser={setCurrentUser}
allUsers={allUsers}
verificationNotice={verificationNotice}
onLoginSuccess={loadPortalData}
/>
</div>

View file

@ -56,7 +56,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
// New User Form State
const [newEmail, setNewEmail] = useState('');
const [newName, setNewName] = useState('');
const [newBranchId, setNewBranchId] = useState<BranchId>('bahrain');
const [newBranchId, setNewBranchId] = useState<BranchId | 'head_office'>('head_office');
const fetchData = async () => {
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) => {
e.preventDefault();
if (!newEmail || !newName) return;
@ -116,15 +116,16 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
email: newEmail,
name: newName,
branchId: newBranchId,
role: 'branch_user',
role: newBranchId === 'head_office' ? 'admin' : 'branch_user',
}),
});
const data = await res.json();
if (res.ok) {
setStatusMsg(`User ${newName} registered. Waiting for admin approval.`);
setStatusMsg(data.message || `User ${newName} registered. Verification link dispatched to ${newEmail}.`);
setNewEmail('');
setNewName('');
fetchData();
onUsersUpdated();
} else {
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
const handleForceResetPassword = async (userId: string, targetName: string) => {
setStatusMsg(null);
@ -313,6 +335,16 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
Admin
</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 ? (
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 px-1.5 rounded font-mono">
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>
{u.role !== 'admin' && (
<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
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"
@ -355,7 +396,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
<span>Force Reset</span>
</button>
)}
{!u.approved && (
{u.role !== 'admin' && !u.approved && (
<button
onClick={() => handleApprove(u.id, true)}
className="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
@ -364,6 +405,7 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
Approve
</button>
)}
{u.role !== 'admin' && (
<button
onClick={() => handleApprove(u.id, false)}
className="px-2.5 py-1 bg-slate-800 hover:bg-rose-900 text-slate-300 hover:text-white font-bold rounded text-[11px] flex items-center gap-1 transition-colors cursor-pointer"
@ -371,9 +413,9 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
<XCircle className="w-3 h-3" />
Remove
</button>
</div>
)}
</div>
</div>
);
})}
</div>
@ -407,9 +449,10 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
<div className="flex justify-between items-center text-xs">
<select
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"
>
<option value="head_office">Head Office (HO)</option>
{BRANCHES_LIST.map((b) => (
<option key={b.id} value={b.id}>
{b.name} ({b.code})

View file

@ -26,6 +26,7 @@ interface AuthModalProps {
currentUser: User;
setCurrentUser: (user: User) => void;
allUsers: User[];
verificationNotice?: string | null;
onLoginSuccess?: () => void;
}
@ -35,6 +36,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
currentUser,
setCurrentUser,
allUsers,
verificationNotice,
onLoginSuccess
}) => {
const [email, setEmail] = useState('');
@ -248,6 +250,13 @@ export const AuthModal: React.FC<AuthModalProps> = ({
</div>
{/* 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 && (
<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" />

View file

@ -31,6 +31,7 @@ import {
BranchId
} from '../types';
import { InteractiveWorldMap } from './InteractiveWorldMap';
import { OpenAINetworkOptimizer } from './OpenAINetworkOptimizer';
interface DashboardAnalyticsProps {
period: string;
@ -163,6 +164,9 @@ export const DashboardAnalytics: React.FC<DashboardAnalyticsProps> = ({ period,
</div>
</div>
{/* OpenAI Quantum Network Balance Sheet Optimizer */}
<OpenAINetworkOptimizer period={period} submissions={submissions} />
{/* Interactive Global World Map View */}
<InteractiveWorldMap period={period} submissions={submissions} />

View file

@ -26,9 +26,13 @@ import {
ChevronLeft,
ChevronRight,
Key,
LogOut
LogOut,
Edit3,
Check,
Palette
} from 'lucide-react';
import { User, BranchId, BRANCHES_LIST } from '../types';
import { HolographicThemeMode } from './HolographicCanvas';
interface NavigationProps {
currentUser: User;
@ -39,6 +43,8 @@ interface NavigationProps {
setActivePeriod: (period: string) => void;
periodsList: string[];
allUsers: User[];
themeMode?: HolographicThemeMode;
onThemeChange?: (theme: HolographicThemeMode) => void;
onLogout?: () => void;
onResetSeed?: () => void;
onOpenAuthModal?: () => void;
@ -51,6 +57,8 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
activeTab,
setActiveTab,
allUsers,
themeMode,
onThemeChange,
onLogout,
onOpenAuthModal
}) => {
@ -59,11 +67,23 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
const isAdmin = currentUser.role === 'admin';
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 (
<aside
className={`transition-all duration-300 ease-in-out ${
isCollapsed ? 'w-16' : 'w-60 lg:w-64'
} bg-slate-900/95 backdrop-blur-2xl border-r border-cyan-500/40 flex flex-col shrink-0 select-none z-30 hidden md:flex hud-corner shadow-[4px_0_25px_rgba(0,0,0,0.6)] h-full overflow-x-hidden`}
} 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 */}
<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
</div>
<div className="overflow-hidden whitespace-nowrap">
<h1 className="text-sm font-bold tracking-tight text-white flex items-center gap-0.5">
EQUITY<span className="text-cyan-400 font-extrabold glow-text-cyan">CORE</span>
{isEditingBrand ? (
<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>
<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>
</div>
</div>
@ -367,6 +414,8 @@ export const HeaderBar: React.FC<NavigationProps> = ({
setActivePeriod,
periodsList,
allUsers,
themeMode = 'amber',
onThemeChange,
onLogout,
onResetSeed,
onOpenAuthModal
@ -375,6 +424,8 @@ export const HeaderBar: React.FC<NavigationProps> = ({
const [showUserMenu, setShowUserMenu] = useState(false);
const [countdown, setCountdown] = useState({ days: 3, hours: 14, mins: 22 });
const brandName = localStorage.getItem('app_brand_title') || 'EQUITY CORE';
useEffect(() => {
const interval = setInterval(() => {
setCountdown((prev) => {
@ -389,7 +440,7 @@ export const HeaderBar: React.FC<NavigationProps> = ({
const currentBranch = BRANCHES_LIST.find((b) => b.id === currentUser.branchId);
return (
<header className="h-14 border-b border-cyan-500/40 bg-slate-900/95 backdrop-blur-2xl flex items-center justify-between px-4 sm:px-6 shrink-0 z-20 w-full shadow-[0_4px_20px_rgba(0,0,0,0.5)] relative">
<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">
{/* Mobile Menu Toggle Button */}
<button
@ -403,7 +454,7 @@ export const HeaderBar: React.FC<NavigationProps> = ({
{/* Brand Badge */}
<div className="flex items-center gap-2">
<div className="w-7 h-7 bg-cyan-500 rounded-lg flex items-center justify-center font-bold text-slate-950 text-xs shadow-[0_0_10px_#06b6d4]">B</div>
<span className="font-bold text-white text-xs tracking-wider md:hidden">EQUITY<span className="text-cyan-400">CORE</span></span>
<span className="font-bold text-white text-xs tracking-wider md:hidden uppercase">{brandName}</span>
</div>
<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 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)]">
<Clock className="w-3.5 h-3.5 text-amber-400 animate-pulse" />
<div className="text-right">

View 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>
);
};

View file

@ -1,43 +1,111 @@
@import "tailwindcss";
@layer utilities {
/* Holographic Glassmorphism */
/* Holographic Glassmorphism & Elevated Card Standouts */
.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);
background: linear-gradient(145deg, rgba(15, 23, 42, 0.98) 0%, rgba(30, 41, 59, 0.95) 100%);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(6, 182, 212, 0.55);
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
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);
}
.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);
border-color: rgba(6, 182, 212, 0.85);
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
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 {
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);
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(236, 72, 153, 0.55);
box-shadow: 0 12px 40px -5px rgba(0, 0, 0, 0.85),
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 {
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);
border-color: rgba(236, 72, 153, 0.85);
box-shadow: 0 16px 50px -5px rgba(0, 0, 0, 0.9),
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 {
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);
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);
border: 1px solid rgba(51, 65, 85, 0.8);
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.8),
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 */

View file

@ -25,12 +25,15 @@ export interface User {
email: string;
name: string;
role: Role;
branchId?: BranchId;
branchId?: BranchId | 'head_office';
approved: boolean;
createdAt: string;
password?: string;
mustChangePassword?: boolean;
isPasswordChanged?: boolean;
isVerified?: boolean;
verificationToken?: string;
verifiedAt?: string;
}
export type PlacementBorrowingType = 'Inter Branch' | 'Inter Bank';