- 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>
305 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|