Files
CurrenciCombo/src/pages/DashboardPage.tsx
Devin AI e83107d71f feat: Solace Bank Group PLC Treasury Management Portal
- Web3 authentication with MetaMask, WalletConnect, Coinbase wallet options
- Demo mode for testing without wallet
- Overview dashboard with KPI cards, asset allocation, positions, accounts, alerts
- Transaction Builder module (full IDE-style drag-and-drop canvas with 28 gap fixes)
- Accounts module with multi-account/subaccount hierarchical structures
- Treasury Management module with positions table and 14-day cash forecast
- Financial Reporting module with IPSAS, US GAAP, IFRS compliance
- Compliance & Risk module with KYC/AML/Sanctions monitoring
- Settlement & Clearing module with DVP/FOP/PVP operations
- Settings with role-based permissions and enterprise controls
- Dark theme professional UI with Solace Bank branding
- HashRouter for static hosting compatibility

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-04-18 10:25:56 -07:00

305 lines
12 KiB
TypeScript

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
TrendingUp, TrendingDown, DollarSign, Activity, AlertTriangle, Clock,
ArrowUpRight, ArrowDownRight, BarChart3, PieChart, Zap, Building2,
Landmark, FileText, Shield, CheckSquare, ChevronRight, RefreshCw
} from 'lucide-react';
import { financialSummary, sampleAccounts, treasuryPositions, complianceAlerts, recentActivity, portalModules } from '../data/portalData';
const formatCurrency = (amount: number, currency = 'USD') => {
if (Math.abs(amount) >= 1_000_000_000) return `${currency === 'USD' ? '$' : ''}${(amount / 1_000_000_000).toFixed(2)}B`;
if (Math.abs(amount) >= 1_000_000) return `${currency === 'USD' ? '$' : ''}${(amount / 1_000_000).toFixed(2)}M`;
if (Math.abs(amount) >= 1_000) return `${currency === 'USD' ? '$' : ''}${(amount / 1_000).toFixed(1)}K`;
return `${currency === 'USD' ? '$' : ''}${amount.toFixed(2)}`;
};
const moduleIcons: Record<string, typeof Zap> = {
'dashboard': BarChart3,
'transaction-builder': Zap,
'accounts': Building2,
'treasury': Landmark,
'reporting': FileText,
'compliance': Shield,
'settlements': CheckSquare,
};
const statusColors: Record<string, string> = {
success: '#22c55e',
warning: '#eab308',
error: '#ef4444',
info: '#3b82f6',
};
const severityColors: Record<string, string> = {
critical: '#ef4444',
high: '#f97316',
medium: '#eab308',
low: '#3b82f6',
};
export default function DashboardPage() {
const navigate = useNavigate();
const [timeRange, setTimeRange] = useState<'1D' | '1W' | '1M' | '3M' | 'YTD'>('1D');
const totalPnL = financialSummary.unrealizedPnL + financialSummary.realizedPnL;
const pnlPositive = totalPnL >= 0;
const assetAllocation = [
{ label: 'Fixed Income', value: 83_900_000, color: '#3b82f6', pct: 39 },
{ label: 'Equities', value: 45_200_000, color: '#22c55e', pct: 21 },
{ label: 'Digital Assets', value: 20_425_000, color: '#a855f7', pct: 10 },
{ label: 'FX', value: 20_250_000, color: '#eab308', pct: 9 },
{ label: 'Commodities', value: 11_500_000, color: '#f97316', pct: 5 },
{ label: 'Cash & Equivalents', value: 33_175_000, color: '#6b7280', pct: 16 },
];
const openAlerts = complianceAlerts.filter(a => a.status !== 'resolved');
return (
<div className="dashboard-page">
<div className="dashboard-header">
<div className="dashboard-header-left">
<h1>Portfolio Overview</h1>
<p className="dashboard-subtitle">Solace Bank Group PLC Consolidated View</p>
</div>
<div className="dashboard-header-right">
<div className="time-range-selector">
{(['1D', '1W', '1M', '3M', 'YTD'] as const).map(range => (
<button
key={range}
className={`time-range-btn ${timeRange === range ? 'active' : ''}`}
onClick={() => setTimeRange(range)}
>
{range}
</button>
))}
</div>
<button className="refresh-btn">
<RefreshCw size={14} />
<span>Refresh</span>
</button>
</div>
</div>
{/* KPI Cards Row */}
<div className="kpi-grid">
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Total Assets (AUM)</span>
<DollarSign size={16} className="kpi-icon" />
</div>
<div className="kpi-value">{formatCurrency(financialSummary.totalAssets)}</div>
<div className="kpi-change positive">
<ArrowUpRight size={12} />
<span>+2.3% from yesterday</span>
</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Net Position</span>
<Activity size={16} className="kpi-icon" />
</div>
<div className="kpi-value">{formatCurrency(financialSummary.netPosition)}</div>
<div className="kpi-change positive">
<ArrowUpRight size={12} />
<span>+1.8% from yesterday</span>
</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Total P&L</span>
{pnlPositive ? <TrendingUp size={16} className="kpi-icon positive" /> : <TrendingDown size={16} className="kpi-icon negative" />}
</div>
<div className={`kpi-value ${pnlPositive ? 'positive' : 'negative'}`}>
{pnlPositive ? '+' : ''}{formatCurrency(totalPnL)}
</div>
<div className="kpi-sub">
<span>Realized: {formatCurrency(financialSummary.realizedPnL)}</span>
<span>Unrealized: {formatCurrency(financialSummary.unrealizedPnL)}</span>
</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Daily Volume</span>
<BarChart3 size={16} className="kpi-icon" />
</div>
<div className="kpi-value">{formatCurrency(financialSummary.dailyVolume)}</div>
<div className="kpi-change negative">
<ArrowDownRight size={12} />
<span>-5.1% from yesterday</span>
</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Pending Settlements</span>
<Clock size={16} className="kpi-icon" />
</div>
<div className="kpi-value">{formatCurrency(financialSummary.pendingSettlements)}</div>
<div className="kpi-sub">
<span>3 DVP · 1 PVP · 2 FOP</span>
</div>
</div>
<div className="kpi-card alert-card">
<div className="kpi-header">
<span className="kpi-label">Active Alerts</span>
<AlertTriangle size={16} className="kpi-icon warning" />
</div>
<div className="kpi-value">{openAlerts.length}</div>
<div className="kpi-sub">
<span style={{ color: '#ef4444' }}>{openAlerts.filter(a => a.severity === 'critical').length} critical</span>
<span style={{ color: '#f97316' }}>{openAlerts.filter(a => a.severity === 'high').length} high</span>
</div>
</div>
</div>
<div className="dashboard-grid">
{/* Asset Allocation */}
<div className="dashboard-card asset-allocation">
<div className="card-header">
<h3><PieChart size={16} /> Asset Allocation</h3>
</div>
<div className="allocation-chart">
<div className="allocation-bar">
{assetAllocation.map(a => (
<div
key={a.label}
className="allocation-segment"
style={{ width: `${a.pct}%`, background: a.color }}
title={`${a.label}: ${a.pct}%`}
/>
))}
</div>
<div className="allocation-legend">
{assetAllocation.map(a => (
<div key={a.label} className="legend-item">
<span className="legend-dot" style={{ background: a.color }} />
<span className="legend-label">{a.label}</span>
<span className="legend-value">{formatCurrency(a.value)}</span>
<span className="legend-pct">{a.pct}%</span>
</div>
))}
</div>
</div>
</div>
{/* Top Positions */}
<div className="dashboard-card positions-card">
<div className="card-header">
<h3><TrendingUp size={16} /> Top Positions</h3>
<button className="card-action" onClick={() => navigate('/treasury')}>View All <ChevronRight size={12} /></button>
</div>
<div className="positions-table">
<div className="positions-header">
<span>Instrument</span>
<span>Market Value</span>
<span>P&L</span>
</div>
{treasuryPositions.slice(0, 6).map(pos => (
<div key={pos.id} className="position-row">
<div className="position-name">
<span className="position-asset-class">{pos.assetClass}</span>
<span>{pos.instrument}</span>
</div>
<span className="mono">{formatCurrency(pos.marketValue)}</span>
<span className={`mono ${pos.unrealizedPnL >= 0 ? 'positive' : 'negative'}`}>
{pos.unrealizedPnL >= 0 ? '+' : ''}{formatCurrency(pos.unrealizedPnL)}
</span>
</div>
))}
</div>
</div>
{/* Accounts Overview */}
<div className="dashboard-card accounts-overview">
<div className="card-header">
<h3><Building2 size={16} /> Accounts</h3>
<button className="card-action" onClick={() => navigate('/accounts')}>Manage <ChevronRight size={12} /></button>
</div>
<div className="accounts-list">
{sampleAccounts.filter(a => !a.parentId).slice(0, 5).map(acc => (
<div key={acc.id} className="account-row">
<div className="account-info">
<span className={`account-type-badge ${acc.type}`}>{acc.type}</span>
<span className="account-name">{acc.name}</span>
</div>
<div className="account-balance">
<span className="mono">
{acc.currency === 'BTC' ? `${acc.balance.toFixed(2)} BTC` : formatCurrency(acc.balance, acc.currency)}
</span>
<span className="account-currency">{acc.currency}</span>
</div>
</div>
))}
</div>
</div>
{/* Compliance Alerts */}
<div className="dashboard-card compliance-card">
<div className="card-header">
<h3><Shield size={16} /> Compliance Alerts</h3>
<button className="card-action" onClick={() => navigate('/compliance')}>View All <ChevronRight size={12} /></button>
</div>
<div className="alerts-list">
{complianceAlerts.filter(a => a.status !== 'resolved').slice(0, 4).map(alert => (
<div key={alert.id} className="alert-row">
<span className="alert-severity" style={{ color: severityColors[alert.severity] }}>
{alert.severity.toUpperCase()}
</span>
<span className="alert-category">{alert.category}</span>
<span className="alert-message">{alert.message}</span>
</div>
))}
</div>
</div>
{/* Recent Activity */}
<div className="dashboard-card activity-card">
<div className="card-header">
<h3><Activity size={16} /> Recent Activity</h3>
</div>
<div className="activity-list">
{recentActivity.map(item => (
<div key={item.id} className="activity-row">
<span className="activity-dot" style={{ background: statusColors[item.status] }} />
<div className="activity-content">
<span className="activity-action">{item.action}</span>
<span className="activity-detail">{item.detail}</span>
</div>
<span className="activity-time">
{item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
))}
</div>
</div>
{/* Quick Access Modules */}
<div className="dashboard-card modules-card">
<div className="card-header">
<h3><Zap size={16} /> Quick Access</h3>
</div>
<div className="modules-grid">
{portalModules.filter(m => m.id !== 'dashboard').map(mod => {
const Icon = moduleIcons[mod.id] || Zap;
return (
<button
key={mod.id}
className="module-card"
onClick={() => navigate(mod.path)}
disabled={mod.status !== 'active'}
>
<Icon size={20} />
<span className="module-name">{mod.name}</span>
<span className="module-desc">{mod.description}</span>
{mod.status === 'coming_soon' && <span className="module-badge">Coming Soon</span>}
</button>
);
})}
</div>
</div>
</div>
</div>
);
}