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>
This commit is contained in:
184
src/pages/AccountsPage.tsx
Normal file
184
src/pages/AccountsPage.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Building2, ChevronRight, ChevronDown, Search, Filter, Plus, Download,
|
||||
ExternalLink, Copy, MoreHorizontal
|
||||
} from 'lucide-react';
|
||||
import { sampleAccounts } from '../data/portalData';
|
||||
import type { Account, AccountType } from '../types/portal';
|
||||
|
||||
const typeColors: Record<AccountType, string> = {
|
||||
operating: '#3b82f6', reserve: '#22c55e', custody: '#a855f7', escrow: '#f97316',
|
||||
settlement: '#06b6d4', nostro: '#eab308', vostro: '#ec4899', collateral: '#6366f1',
|
||||
treasury: '#14b8a6', crypto_wallet: '#8b5cf6', stablecoin: '#10b981', omnibus: '#64748b',
|
||||
};
|
||||
|
||||
const formatBalance = (amount: number, currency: string) => {
|
||||
if (currency === 'BTC') return `${amount.toFixed(4)} BTC`;
|
||||
if (currency === 'USDC') return `$${amount.toLocaleString()}`;
|
||||
const sym = currency === 'USD' ? '$' : currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : '';
|
||||
if (Math.abs(amount) >= 1_000_000) return `${sym}${(amount / 1_000_000).toFixed(2)}M`;
|
||||
return `${sym}${amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
function AccountRow({ account, level = 0 }: { account: Account; level?: number }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasChildren = account.subaccounts && account.subaccounts.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`account-table-row level-${level}`} style={{ paddingLeft: `${16 + level * 24}px` }}>
|
||||
<div className="account-table-name">
|
||||
{hasChildren ? (
|
||||
<button className="expand-btn" onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="expand-placeholder" />
|
||||
)}
|
||||
<span className="account-type-dot" style={{ background: typeColors[account.type] }} />
|
||||
<div>
|
||||
<span className="account-name-text">{account.name}</span>
|
||||
<span className="account-type-label">{account.type.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="account-table-cell currency">{account.currency}</div>
|
||||
<div className="account-table-cell mono balance">{formatBalance(account.balance, account.currency)}</div>
|
||||
<div className="account-table-cell mono available">{formatBalance(account.availableBalance, account.currency)}</div>
|
||||
<div className="account-table-cell">
|
||||
<span className={`account-status-badge ${account.status}`}>{account.status}</span>
|
||||
</div>
|
||||
<div className="account-table-cell identifier">
|
||||
{account.iban && <span className="mono small">{account.iban}</span>}
|
||||
{account.walletAddress && <span className="mono small">{account.walletAddress.slice(0, 10)}...</span>}
|
||||
{account.swift && <span className="swift-badge">{account.swift}</span>}
|
||||
</div>
|
||||
<div className="account-table-cell">
|
||||
<span className="mono small">{account.lastActivity.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
<div className="account-table-cell actions">
|
||||
<button className="row-action-btn" title="View Details"><ExternalLink size={12} /></button>
|
||||
<button className="row-action-btn" title="Copy ID"><Copy size={12} /></button>
|
||||
<button className="row-action-btn" title="More"><MoreHorizontal size={12} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && hasChildren && account.subaccounts!.map(sub => (
|
||||
<AccountRow key={sub.id} account={sub} level={level + 1} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccountsPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [view, setView] = useState<'tree' | 'flat'>('tree');
|
||||
|
||||
const allAccounts = view === 'flat'
|
||||
? sampleAccounts.flatMap(a => [a, ...(a.subaccounts || [])])
|
||||
: sampleAccounts;
|
||||
|
||||
const filtered = allAccounts.filter(a => {
|
||||
const matchSearch = a.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.currency.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.type.includes(search.toLowerCase());
|
||||
const matchType = typeFilter === 'all' || a.type === typeFilter;
|
||||
return matchSearch && matchType && (view === 'flat' || !a.parentId);
|
||||
});
|
||||
|
||||
const totalBalance = sampleAccounts.reduce((sum, a) => {
|
||||
if (a.currency === 'USD' || a.currency === 'USDC') return sum + a.balance;
|
||||
if (a.currency === 'EUR') return sum + a.balance * 1.08;
|
||||
if (a.currency === 'GBP') return sum + a.balance * 1.27;
|
||||
if (a.currency === 'BTC') return sum + a.balance * 67_000;
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="accounts-page">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1><Building2 size={24} /> Account Management</h1>
|
||||
<p className="page-subtitle">Multi-account and subaccount structures with consolidated views</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<button className="btn-secondary"><Download size={14} /> Export</button>
|
||||
<button className="btn-primary"><Plus size={14} /> New Account</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="accounts-summary">
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Total Accounts</span>
|
||||
<span className="summary-value">{sampleAccounts.length + sampleAccounts.reduce((c, a) => c + (a.subaccounts?.length || 0), 0)}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Consolidated Balance (USD eq.)</span>
|
||||
<span className="summary-value">${(totalBalance / 1_000_000).toFixed(2)}M</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Active</span>
|
||||
<span className="summary-value green">{sampleAccounts.filter(a => a.status === 'active').length}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Frozen</span>
|
||||
<span className="summary-value orange">{sampleAccounts.filter(a => a.status === 'frozen').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="table-toolbar">
|
||||
<div className="table-toolbar-left">
|
||||
<div className="search-input-wrapper">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search accounts..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<Filter size={14} />
|
||||
<select value={typeFilter} onChange={e => setTypeFilter(e.target.value)}>
|
||||
<option value="all">All Types</option>
|
||||
<option value="operating">Operating</option>
|
||||
<option value="treasury">Treasury</option>
|
||||
<option value="custody">Custody</option>
|
||||
<option value="settlement">Settlement</option>
|
||||
<option value="nostro">Nostro</option>
|
||||
<option value="escrow">Escrow</option>
|
||||
<option value="collateral">Collateral</option>
|
||||
<option value="stablecoin">Stablecoin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-toolbar-right">
|
||||
<div className="view-toggle">
|
||||
<button className={view === 'tree' ? 'active' : ''} onClick={() => setView('tree')}>Tree</button>
|
||||
<button className={view === 'flat' ? 'active' : ''} onClick={() => setView('flat')}>Flat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Table */}
|
||||
<div className="account-table">
|
||||
<div className="account-table-header">
|
||||
<div className="account-table-name">Account</div>
|
||||
<div className="account-table-cell currency">Currency</div>
|
||||
<div className="account-table-cell balance">Balance</div>
|
||||
<div className="account-table-cell available">Available</div>
|
||||
<div className="account-table-cell">Status</div>
|
||||
<div className="account-table-cell identifier">Identifier</div>
|
||||
<div className="account-table-cell">Last Activity</div>
|
||||
<div className="account-table-cell actions" />
|
||||
</div>
|
||||
<div className="account-table-body">
|
||||
{filtered.map(acc => (
|
||||
<AccountRow key={acc.id} account={acc} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
145
src/pages/CompliancePage.tsx
Normal file
145
src/pages/CompliancePage.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useState } from 'react';
|
||||
import { Shield, AlertTriangle, CheckCircle2, Clock, Filter, Download, Eye, UserCheck } from 'lucide-react';
|
||||
import { complianceAlerts } from '../data/portalData';
|
||||
|
||||
const severityColors: Record<string, string> = {
|
||||
critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#3b82f6',
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
open: '#ef4444', acknowledged: '#eab308', resolved: '#22c55e',
|
||||
};
|
||||
|
||||
const complianceMetrics = [
|
||||
{ label: 'KYC Verified', value: '142', total: '145', pct: 98, color: '#22c55e' },
|
||||
{ label: 'AML Screening', value: 'Active', total: '47 rules', pct: 100, color: '#3b82f6' },
|
||||
{ label: 'Sanctions Check', value: 'Current', total: 'OFAC/EU/UN', pct: 100, color: '#a855f7' },
|
||||
{ label: 'Travel Rule', value: '98.5%', total: 'compliant', pct: 98.5, color: '#14b8a6' },
|
||||
];
|
||||
|
||||
const regulatoryFrameworks = [
|
||||
{ name: 'FATF Travel Rule', status: 'compliant', lastReview: '2024-03-15', nextReview: '2024-06-15' },
|
||||
{ name: 'MiCA (EU)', status: 'compliant', lastReview: '2024-02-28', nextReview: '2024-05-28' },
|
||||
{ name: 'Bank Secrecy Act (US)', status: 'compliant', lastReview: '2024-03-01', nextReview: '2024-06-01' },
|
||||
{ name: 'FCA Regulations (UK)', status: 'review_needed', lastReview: '2024-01-15', nextReview: '2024-04-15' },
|
||||
{ name: 'MAS Guidelines (SG)', status: 'compliant', lastReview: '2024-03-10', nextReview: '2024-06-10' },
|
||||
{ name: 'JFSA Standards (JP)', status: 'compliant', lastReview: '2024-02-20', nextReview: '2024-05-20' },
|
||||
];
|
||||
|
||||
export default function CompliancePage() {
|
||||
const [severityFilter, setSeverityFilter] = useState('all');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
const filtered = complianceAlerts.filter(a => {
|
||||
const matchSev = severityFilter === 'all' || a.severity === severityFilter;
|
||||
const matchStatus = statusFilter === 'all' || a.status === statusFilter;
|
||||
return matchSev && matchStatus;
|
||||
});
|
||||
|
||||
const openCount = complianceAlerts.filter(a => a.status === 'open').length;
|
||||
const criticalCount = complianceAlerts.filter(a => a.severity === 'critical' && a.status !== 'resolved').length;
|
||||
|
||||
return (
|
||||
<div className="compliance-page">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1><Shield size={24} /> Compliance & Risk Management</h1>
|
||||
<p className="page-subtitle">Regulatory compliance monitoring, AML/KYC oversight, and risk controls</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<button className="btn-secondary"><Download size={14} /> Export Report</button>
|
||||
<button className="btn-primary"><UserCheck size={14} /> Run Full Scan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Metrics */}
|
||||
<div className="compliance-metrics">
|
||||
{complianceMetrics.map(m => (
|
||||
<div key={m.label} className="metric-card">
|
||||
<div className="metric-header">
|
||||
<span className="metric-label">{m.label}</span>
|
||||
<CheckCircle2 size={14} color={m.color} />
|
||||
</div>
|
||||
<div className="metric-value" style={{ color: m.color }}>{m.value}</div>
|
||||
<div className="metric-sub">{m.total}</div>
|
||||
<div className="metric-bar">
|
||||
<div className="metric-bar-fill" style={{ width: `${m.pct}%`, background: m.color }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="compliance-grid">
|
||||
{/* Alerts */}
|
||||
<div className="dashboard-card alerts-table-card">
|
||||
<div className="card-header">
|
||||
<h3><AlertTriangle size={16} /> Active Alerts ({openCount} open, {criticalCount} critical)</h3>
|
||||
<div className="card-header-actions">
|
||||
<div className="filter-group">
|
||||
<Filter size={12} />
|
||||
<select value={severityFilter} onChange={e => setSeverityFilter(e.target.value)}>
|
||||
<option value="all">All Severity</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<select value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
|
||||
<option value="all">All Status</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="acknowledged">Acknowledged</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="alerts-table">
|
||||
{filtered.map(alert => (
|
||||
<div key={alert.id} className="alert-table-row">
|
||||
<span className="alert-sev-badge" style={{ background: severityColors[alert.severity] + '20', color: severityColors[alert.severity], borderColor: severityColors[alert.severity] + '40' }}>
|
||||
{alert.severity.toUpperCase()}
|
||||
</span>
|
||||
<span className="alert-cat">{alert.category}</span>
|
||||
<span className="alert-msg">{alert.message}</span>
|
||||
<span className="alert-status-badge" style={{ color: statusColors[alert.status] }}>
|
||||
{alert.status === 'resolved' ? <CheckCircle2 size={10} /> : alert.status === 'acknowledged' ? <Eye size={10} /> : <Clock size={10} />}
|
||||
{alert.status}
|
||||
</span>
|
||||
<span className="alert-time mono">
|
||||
{alert.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
{alert.assignedTo && <span className="alert-assigned">{alert.assignedTo}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regulatory Frameworks */}
|
||||
<div className="dashboard-card regulatory-card">
|
||||
<div className="card-header">
|
||||
<h3><Shield size={16} /> Regulatory Frameworks</h3>
|
||||
</div>
|
||||
<div className="regulatory-list">
|
||||
{regulatoryFrameworks.map(fw => (
|
||||
<div key={fw.name} className="regulatory-row">
|
||||
<div className="regulatory-info">
|
||||
<span className="regulatory-name">{fw.name}</span>
|
||||
<span className={`regulatory-status ${fw.status}`}>
|
||||
{fw.status === 'compliant' ? <CheckCircle2 size={10} /> : <AlertTriangle size={10} />}
|
||||
{fw.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="regulatory-dates">
|
||||
<span className="small">Last: {fw.lastReview}</span>
|
||||
<span className="small">Next: {fw.nextReview}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
src/pages/DashboardPage.tsx
Normal file
304
src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
189
src/pages/LoginPage.tsx
Normal file
189
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Shield, Wallet, ArrowRight, Globe, Lock, Zap, TrendingUp, Building2, ChevronRight } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const { connectWallet, loading, error } = useAuth();
|
||||
const [connecting, setConnecting] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = async (provider: 'metamask' | 'walletconnect' | 'coinbase') => {
|
||||
setConnecting(provider);
|
||||
await connectWallet(provider);
|
||||
setConnecting(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-bg-grid" />
|
||||
<div className="login-bg-glow" />
|
||||
|
||||
<div className="login-container">
|
||||
<div className="login-left">
|
||||
<div className="login-brand">
|
||||
<div className="login-logo">
|
||||
<Building2 size={32} />
|
||||
<div>
|
||||
<h1>Solace Bank Group</h1>
|
||||
<span className="login-plc">PLC</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="login-tagline">Enterprise Treasury Management Portal</p>
|
||||
</div>
|
||||
|
||||
<div className="login-features">
|
||||
<div className="login-feature">
|
||||
<div className="login-feature-icon">
|
||||
<TrendingUp size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3>Multi-Asset Treasury</h3>
|
||||
<p>Consolidated views across fiat, digital assets, securities, and commodities</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="login-feature">
|
||||
<div className="login-feature-icon">
|
||||
<Shield size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3>Regulatory Compliance</h3>
|
||||
<p>IPSAS, US GAAP, and IFRS compliant reporting frameworks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="login-feature">
|
||||
<div className="login-feature-icon">
|
||||
<Globe size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3>Global Settlement</h3>
|
||||
<p>Cross-border payment orchestration with real-time settlement tracking</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="login-feature">
|
||||
<div className="login-feature-icon">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3>Web3 Security</h3>
|
||||
<p>Cryptographic wallet authentication with enterprise-grade access controls</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-compliance-badges">
|
||||
<span className="compliance-badge">IPSAS</span>
|
||||
<span className="compliance-badge">US GAAP</span>
|
||||
<span className="compliance-badge">IFRS</span>
|
||||
<span className="compliance-badge">ISO 20022</span>
|
||||
<span className="compliance-badge">SOC 2</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-right">
|
||||
<div className="login-card">
|
||||
<div className="login-card-header">
|
||||
<Wallet size={24} />
|
||||
<h2>Connect Wallet</h2>
|
||||
<p>Authenticate with your Web3 wallet to access the portal</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="login-error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="login-wallets">
|
||||
<button
|
||||
className={`wallet-option ${connecting === 'metamask' ? 'connecting' : ''}`}
|
||||
onClick={() => handleConnect('metamask')}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="wallet-option-left">
|
||||
<div className="wallet-icon metamask">
|
||||
<span>🦊</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="wallet-name">MetaMask</span>
|
||||
<span className="wallet-desc">Browser extension wallet</span>
|
||||
</div>
|
||||
</div>
|
||||
{connecting === 'metamask' ? (
|
||||
<div className="wallet-spinner" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="wallet-arrow" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`wallet-option ${connecting === 'walletconnect' ? 'connecting' : ''}`}
|
||||
onClick={() => handleConnect('walletconnect')}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="wallet-option-left">
|
||||
<div className="wallet-icon walletconnect">
|
||||
<span>🔗</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="wallet-name">WalletConnect</span>
|
||||
<span className="wallet-desc">Scan QR code to connect</span>
|
||||
</div>
|
||||
</div>
|
||||
{connecting === 'walletconnect' ? (
|
||||
<div className="wallet-spinner" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="wallet-arrow" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`wallet-option ${connecting === 'coinbase' ? 'connecting' : ''}`}
|
||||
onClick={() => handleConnect('coinbase')}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="wallet-option-left">
|
||||
<div className="wallet-icon coinbase">
|
||||
<span>🔵</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="wallet-name">Coinbase Wallet</span>
|
||||
<span className="wallet-desc">Coinbase self-custody wallet</span>
|
||||
</div>
|
||||
</div>
|
||||
{connecting === 'coinbase' ? (
|
||||
<div className="wallet-spinner" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="wallet-arrow" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="login-divider">
|
||||
<span>or</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="login-demo-btn"
|
||||
onClick={() => handleConnect('metamask')}
|
||||
disabled={loading}
|
||||
>
|
||||
<Zap size={16} />
|
||||
<span>Enter Demo Mode</span>
|
||||
<ArrowRight size={14} />
|
||||
</button>
|
||||
|
||||
<p className="login-terms">
|
||||
By connecting, you agree to the Terms of Service and acknowledge
|
||||
that Solace Bank Group PLC processes authentication via
|
||||
cryptographic signature verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-security-note">
|
||||
<Lock size={12} />
|
||||
<span>End-to-end encrypted · No private keys stored · SOC 2 Type II certified</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
180
src/pages/ReportingPage.tsx
Normal file
180
src/pages/ReportingPage.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState } from 'react';
|
||||
import { FileText, Download, Filter, Plus, Eye, Clock, CheckCircle2, AlertTriangle, Send } from 'lucide-react';
|
||||
import { reportConfigs } from '../data/portalData';
|
||||
import type { ReportingStandard } from '../types/portal';
|
||||
|
||||
const standardColors: Record<ReportingStandard, string> = {
|
||||
IPSAS: '#a855f7',
|
||||
US_GAAP: '#3b82f6',
|
||||
IFRS: '#22c55e',
|
||||
};
|
||||
|
||||
const statusIcons: Record<string, typeof Clock> = {
|
||||
draft: Clock,
|
||||
generated: AlertTriangle,
|
||||
reviewed: Eye,
|
||||
published: CheckCircle2,
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
draft: '#6b7280',
|
||||
generated: '#eab308',
|
||||
reviewed: '#3b82f6',
|
||||
published: '#22c55e',
|
||||
};
|
||||
|
||||
export default function ReportingPage() {
|
||||
const [standardFilter, setStandardFilter] = useState<string>('all');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [activeStandard, setActiveStandard] = useState<ReportingStandard>('IFRS');
|
||||
|
||||
const filtered = reportConfigs.filter(r => {
|
||||
const matchStandard = standardFilter === 'all' || r.standard === standardFilter;
|
||||
const matchType = typeFilter === 'all' || r.type === typeFilter;
|
||||
return matchStandard && matchType;
|
||||
});
|
||||
|
||||
const standardDetails: Record<ReportingStandard, { full: string; description: string; keyStatements: string[]; jurisdiction: string }> = {
|
||||
IPSAS: {
|
||||
full: 'International Public Sector Accounting Standards',
|
||||
description: 'Accrual-based accounting standards for public sector entities, issued by the IPSASB. Ensures transparency and accountability in government financial reporting.',
|
||||
keyStatements: ['Statement of Financial Position', 'Statement of Financial Performance', 'Statement of Changes in Net Assets', 'Cash Flow Statement', 'Budget Comparison Statement'],
|
||||
jurisdiction: 'International (Public Sector)',
|
||||
},
|
||||
US_GAAP: {
|
||||
full: 'United States Generally Accepted Accounting Principles',
|
||||
description: 'Comprehensive accounting framework issued by FASB, mandatory for US public companies and widely adopted by financial institutions.',
|
||||
keyStatements: ['Balance Sheet', 'Income Statement', 'Statement of Cash Flows', 'Statement of Stockholders\' Equity', 'Notes to Financial Statements'],
|
||||
jurisdiction: 'United States',
|
||||
},
|
||||
IFRS: {
|
||||
full: 'International Financial Reporting Standards',
|
||||
description: 'Global accounting standards issued by the IASB, adopted by 140+ jurisdictions. Principle-based framework for transparent financial reporting.',
|
||||
keyStatements: ['Statement of Financial Position', 'Statement of Profit or Loss', 'Statement of Comprehensive Income', 'Statement of Cash Flows', 'Statement of Changes in Equity'],
|
||||
jurisdiction: 'International (140+ jurisdictions)',
|
||||
},
|
||||
};
|
||||
|
||||
const detail = standardDetails[activeStandard];
|
||||
|
||||
return (
|
||||
<div className="reporting-page">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1><FileText size={24} /> Financial Reporting</h1>
|
||||
<p className="page-subtitle">IPSAS, US GAAP, and IFRS compliant reporting frameworks</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<button className="btn-secondary"><Download size={14} /> Export All</button>
|
||||
<button className="btn-primary"><Plus size={14} /> Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Standards Overview */}
|
||||
<div className="standards-tabs">
|
||||
{(['IPSAS', 'US_GAAP', 'IFRS'] as ReportingStandard[]).map(std => (
|
||||
<button
|
||||
key={std}
|
||||
className={`standard-tab ${activeStandard === std ? 'active' : ''}`}
|
||||
onClick={() => setActiveStandard(std)}
|
||||
style={activeStandard === std ? { borderColor: standardColors[std], color: standardColors[std] } : {}}
|
||||
>
|
||||
<span className="standard-dot" style={{ background: standardColors[std] }} />
|
||||
{std.replace('_', ' ')}
|
||||
<span className="standard-count">{reportConfigs.filter(r => r.standard === std).length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="standard-detail-card" style={{ borderColor: standardColors[activeStandard] + '40' }}>
|
||||
<div className="standard-detail-header">
|
||||
<div>
|
||||
<h3 style={{ color: standardColors[activeStandard] }}>{activeStandard.replace('_', ' ')}</h3>
|
||||
<p className="standard-full-name">{detail.full}</p>
|
||||
</div>
|
||||
<span className="jurisdiction-badge">{detail.jurisdiction}</span>
|
||||
</div>
|
||||
<p className="standard-description">{detail.description}</p>
|
||||
<div className="key-statements">
|
||||
<span className="key-statements-label">Key Financial Statements:</span>
|
||||
<div className="statements-list">
|
||||
{detail.keyStatements.map(stmt => (
|
||||
<span key={stmt} className="statement-badge">{stmt}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reports Table */}
|
||||
<div className="dashboard-card reports-card">
|
||||
<div className="card-header">
|
||||
<h3>Generated Reports</h3>
|
||||
<div className="card-header-actions">
|
||||
<div className="filter-group">
|
||||
<Filter size={12} />
|
||||
<select value={standardFilter} onChange={e => setStandardFilter(e.target.value)}>
|
||||
<option value="all">All Standards</option>
|
||||
<option value="IPSAS">IPSAS</option>
|
||||
<option value="US_GAAP">US GAAP</option>
|
||||
<option value="IFRS">IFRS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<select value={typeFilter} onChange={e => setTypeFilter(e.target.value)}>
|
||||
<option value="all">All Types</option>
|
||||
<option value="balance_sheet">Balance Sheet</option>
|
||||
<option value="income_statement">Income Statement</option>
|
||||
<option value="cash_flow">Cash Flow</option>
|
||||
<option value="trial_balance">Trial Balance</option>
|
||||
<option value="regulatory">Regulatory</option>
|
||||
<option value="position_summary">Position Summary</option>
|
||||
<option value="risk_exposure">Risk Exposure</option>
|
||||
<option value="compliance_summary">Compliance Summary</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="reports-table">
|
||||
<div className="reports-table-header">
|
||||
<span>Report Name</span>
|
||||
<span>Standard</span>
|
||||
<span>Type</span>
|
||||
<span>Period</span>
|
||||
<span>Status</span>
|
||||
<span>Generated</span>
|
||||
<span>By</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
{filtered.map(report => {
|
||||
const StatusIcon = statusIcons[report.status] || Clock;
|
||||
return (
|
||||
<div key={report.id} className="reports-table-row">
|
||||
<span className="report-name">{report.name}</span>
|
||||
<span>
|
||||
<span className="standard-badge" style={{ color: standardColors[report.standard], borderColor: standardColors[report.standard] + '40' }}>
|
||||
{report.standard.replace('_', ' ')}
|
||||
</span>
|
||||
</span>
|
||||
<span className="report-type">{report.type.replace(/_/g, ' ')}</span>
|
||||
<span className="report-period">{report.period}</span>
|
||||
<span>
|
||||
<span className="report-status" style={{ color: statusColors[report.status] }}>
|
||||
<StatusIcon size={12} />
|
||||
{report.status}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mono small">{report.generatedAt ? report.generatedAt.toLocaleDateString() : '—'}</span>
|
||||
<span className="small">{report.generatedBy || '—'}</span>
|
||||
<span className="report-actions">
|
||||
<button className="row-action-btn" title="View"><Eye size={12} /></button>
|
||||
<button className="row-action-btn" title="Download"><Download size={12} /></button>
|
||||
<button className="row-action-btn" title="Submit"><Send size={12} /></button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/pages/SettlementsPage.tsx
Normal file
148
src/pages/SettlementsPage.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react';
|
||||
import { CheckSquare, Filter, Download, Clock, CheckCircle2, XCircle, ArrowUpDown } from 'lucide-react';
|
||||
import { settlementRecords } from '../data/portalData';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: '#eab308', matched: '#3b82f6', affirmed: '#a855f7',
|
||||
settled: '#22c55e', failed: '#ef4444', cancelled: '#6b7280',
|
||||
};
|
||||
|
||||
const statusIcons: Record<string, typeof Clock> = {
|
||||
pending: Clock, matched: CheckCircle2, affirmed: CheckCircle2,
|
||||
settled: CheckCircle2, failed: XCircle, cancelled: XCircle,
|
||||
};
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
DVP: '#3b82f6', FOP: '#22c55e', PVP: '#a855f7', internal: '#6b7280',
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number, currency: string) => {
|
||||
const sym = currency === 'USD' ? '$' : currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : currency === 'JPY' ? '¥' : '';
|
||||
if (Math.abs(amount) >= 1_000_000) return `${sym}${(amount / 1_000_000).toFixed(2)}M`;
|
||||
return `${sym}${amount.toLocaleString()}`;
|
||||
};
|
||||
|
||||
export default function SettlementsPage() {
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [typeFilter, setTypeFilter] = useState('all');
|
||||
const [sortBy, setSortBy] = useState<'date' | 'amount'>('date');
|
||||
|
||||
const filtered = settlementRecords
|
||||
.filter(s => (statusFilter === 'all' || s.status === statusFilter) && (typeFilter === 'all' || s.type === typeFilter))
|
||||
.sort((a, b) => sortBy === 'date' ? b.settlementDate.getTime() - a.settlementDate.getTime() : b.amount - a.amount);
|
||||
|
||||
const pendingCount = settlementRecords.filter(s => ['pending', 'matched', 'affirmed'].includes(s.status)).length;
|
||||
const settledCount = settlementRecords.filter(s => s.status === 'settled').length;
|
||||
const failedCount = settlementRecords.filter(s => s.status === 'failed').length;
|
||||
const totalPending = settlementRecords
|
||||
.filter(s => ['pending', 'matched', 'affirmed'].includes(s.status))
|
||||
.reduce((sum, s) => sum + s.amount, 0);
|
||||
|
||||
return (
|
||||
<div className="settlements-page">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1><CheckSquare size={24} /> Settlement & Clearing</h1>
|
||||
<p className="page-subtitle">Settlement lifecycle tracking, DVP/FOP/PVP operations, and CSD integration</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<button className="btn-secondary"><Download size={14} /> Export</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settlements-summary">
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Pending</span>
|
||||
<span className="summary-value orange">{pendingCount}</span>
|
||||
<span className="summary-sub">{formatCurrency(totalPending, 'USD')} total</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Settled</span>
|
||||
<span className="summary-value green">{settledCount}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Failed</span>
|
||||
<span className="summary-value red">{failedCount}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Settlement Rate</span>
|
||||
<span className="summary-value">{settledCount > 0 ? ((settledCount / (settledCount + failedCount)) * 100).toFixed(0) : 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h3>Settlement Queue</h3>
|
||||
<div className="card-header-actions">
|
||||
<div className="filter-group">
|
||||
<Filter size={12} />
|
||||
<select value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
|
||||
<option value="all">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="matched">Matched</option>
|
||||
<option value="affirmed">Affirmed</option>
|
||||
<option value="settled">Settled</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<select value={typeFilter} onChange={e => setTypeFilter(e.target.value)}>
|
||||
<option value="all">All Types</option>
|
||||
<option value="DVP">DVP</option>
|
||||
<option value="FOP">FOP</option>
|
||||
<option value="PVP">PVP</option>
|
||||
<option value="internal">Internal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<ArrowUpDown size={12} />
|
||||
<select value={sortBy} onChange={e => setSortBy(e.target.value as 'date' | 'amount')}>
|
||||
<option value="date">Sort by Date</option>
|
||||
<option value="amount">Sort by Amount</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settlements-table">
|
||||
<div className="settlements-table-header">
|
||||
<span>TX ID</span>
|
||||
<span>Type</span>
|
||||
<span>Status</span>
|
||||
<span>Amount</span>
|
||||
<span>Currency</span>
|
||||
<span>Counterparty</span>
|
||||
<span>Settlement Date</span>
|
||||
<span>Value Date</span>
|
||||
<span>CSD</span>
|
||||
</div>
|
||||
{filtered.map(record => {
|
||||
const StatusIcon = statusIcons[record.status] || Clock;
|
||||
return (
|
||||
<div key={record.id} className="settlements-table-row">
|
||||
<span className="mono">{record.txId}</span>
|
||||
<span>
|
||||
<span className="type-badge" style={{ color: typeColors[record.type], borderColor: typeColors[record.type] + '40' }}>
|
||||
{record.type}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="settlement-status" style={{ color: statusColors[record.status] }}>
|
||||
<StatusIcon size={12} />
|
||||
{record.status}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mono">{formatCurrency(record.amount, record.currency)}</span>
|
||||
<span>{record.currency}</span>
|
||||
<span>{record.counterparty}</span>
|
||||
<span className="mono small">{record.settlementDate.toLocaleDateString()}</span>
|
||||
<span className="mono small">{record.valueDate.toLocaleDateString()}</span>
|
||||
<span className="csd-badge">{record.csd || '—'}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
src/pages/TreasuryPage.tsx
Normal file
153
src/pages/TreasuryPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useState } from 'react';
|
||||
import { Landmark, TrendingUp, TrendingDown, Download, Filter, ArrowUpDown, RefreshCw } from 'lucide-react';
|
||||
import { treasuryPositions, cashForecasts } from '../data/portalData';
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
if (Math.abs(amount) >= 1_000_000) return `$${(amount / 1_000_000).toFixed(2)}M`;
|
||||
if (Math.abs(amount) >= 1_000) return `$${(amount / 1_000).toFixed(1)}K`;
|
||||
return `$${amount.toFixed(2)}`;
|
||||
};
|
||||
|
||||
export default function TreasuryPage() {
|
||||
const [assetFilter, setAssetFilter] = useState('all');
|
||||
const [sortBy, setSortBy] = useState<'value' | 'pnl' | 'name'>('value');
|
||||
|
||||
const assetClasses = [...new Set(treasuryPositions.map(p => p.assetClass))];
|
||||
|
||||
const filtered = treasuryPositions
|
||||
.filter(p => assetFilter === 'all' || p.assetClass === assetFilter)
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'value') return b.marketValue - a.marketValue;
|
||||
if (sortBy === 'pnl') return b.unrealizedPnL - a.unrealizedPnL;
|
||||
return a.instrument.localeCompare(b.instrument);
|
||||
});
|
||||
|
||||
const totalMarketValue = treasuryPositions.reduce((s, p) => s + p.marketValue, 0);
|
||||
const totalCostBasis = treasuryPositions.reduce((s, p) => s + p.costBasis, 0);
|
||||
const totalPnL = treasuryPositions.reduce((s, p) => s + p.unrealizedPnL, 0);
|
||||
|
||||
const forecastData = cashForecasts.slice(0, 14);
|
||||
|
||||
return (
|
||||
<div className="treasury-page">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1><Landmark size={24} /> Treasury Management</h1>
|
||||
<p className="page-subtitle">Position monitoring, cash management, and portfolio analytics</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<button className="btn-secondary"><RefreshCw size={14} /> Refresh Prices</button>
|
||||
<button className="btn-secondary"><Download size={14} /> Export Positions</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Portfolio Summary */}
|
||||
<div className="treasury-summary">
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Total Market Value</span>
|
||||
<span className="summary-value">{formatCurrency(totalMarketValue)}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Total Cost Basis</span>
|
||||
<span className="summary-value">{formatCurrency(totalCostBasis)}</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Unrealized P&L</span>
|
||||
<span className={`summary-value ${totalPnL >= 0 ? 'green' : 'red'}`}>
|
||||
{totalPnL >= 0 ? '+' : ''}{formatCurrency(totalPnL)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<span className="summary-label">Return</span>
|
||||
<span className={`summary-value ${totalPnL >= 0 ? 'green' : 'red'}`}>
|
||||
{totalPnL >= 0 ? '+' : ''}{((totalPnL / totalCostBasis) * 100).toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="treasury-grid">
|
||||
{/* Positions Table */}
|
||||
<div className="dashboard-card positions-table-card">
|
||||
<div className="card-header">
|
||||
<h3><TrendingUp size={16} /> Positions</h3>
|
||||
<div className="card-header-actions">
|
||||
<div className="filter-group">
|
||||
<Filter size={12} />
|
||||
<select value={assetFilter} onChange={e => setAssetFilter(e.target.value)}>
|
||||
<option value="all">All Asset Classes</option>
|
||||
{assetClasses.map(ac => (
|
||||
<option key={ac} value={ac}>{ac}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<ArrowUpDown size={12} />
|
||||
<select value={sortBy} onChange={e => setSortBy(e.target.value as 'value' | 'pnl' | 'name')}>
|
||||
<option value="value">Sort by Value</option>
|
||||
<option value="pnl">Sort by P&L</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="treasury-table">
|
||||
<div className="treasury-table-header">
|
||||
<span>Instrument</span>
|
||||
<span>Asset Class</span>
|
||||
<span>Quantity</span>
|
||||
<span>Market Value</span>
|
||||
<span>Cost Basis</span>
|
||||
<span>Unrealized P&L</span>
|
||||
<span>Custodian</span>
|
||||
</div>
|
||||
{filtered.map(pos => (
|
||||
<div key={pos.id} className="treasury-table-row">
|
||||
<span className="instrument-name">{pos.instrument}</span>
|
||||
<span><span className="asset-class-badge">{pos.assetClass}</span></span>
|
||||
<span className="mono">{pos.quantity.toLocaleString()}</span>
|
||||
<span className="mono">{formatCurrency(pos.marketValue)}</span>
|
||||
<span className="mono">{formatCurrency(pos.costBasis)}</span>
|
||||
<span className={`mono ${pos.unrealizedPnL >= 0 ? 'positive' : 'negative'}`}>
|
||||
{pos.unrealizedPnL >= 0 ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
|
||||
{' '}{pos.unrealizedPnL >= 0 ? '+' : ''}{formatCurrency(pos.unrealizedPnL)}
|
||||
</span>
|
||||
<span className="custodian-name">{pos.custodian}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cash Forecast */}
|
||||
<div className="dashboard-card forecast-card">
|
||||
<div className="card-header">
|
||||
<h3>📈 14-Day Cash Forecast</h3>
|
||||
</div>
|
||||
<div className="forecast-chart">
|
||||
{forecastData.map((f, i) => {
|
||||
const maxVal = Math.max(...forecastData.map(x => x.projected));
|
||||
const minVal = Math.min(...forecastData.map(x => x.projected));
|
||||
const range = maxVal - minVal || 1;
|
||||
const height = ((f.projected - minVal) / range) * 80 + 20;
|
||||
return (
|
||||
<div key={i} className="forecast-bar-wrapper">
|
||||
<div
|
||||
className={`forecast-bar ${f.actual ? 'actual' : ''}`}
|
||||
style={{ height: `${height}%` }}
|
||||
title={`${f.date.toLocaleDateString()}: $${(f.projected / 1_000_000).toFixed(1)}M`}
|
||||
/>
|
||||
<span className="forecast-label">
|
||||
{f.date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="forecast-legend">
|
||||
<span><span className="legend-dot actual" /> Actual</span>
|
||||
<span><span className="legend-dot projected" /> Projected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user