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_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=
|
||||
|
|
|
|||
3
bun.lock
3
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
358
server.ts
|
|
@ -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)) {
|
||||
|
|
|
|||
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 [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>
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
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";
|
||||
|
||||
@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 */
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue