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:
Devin AI
2026-04-18 17:17:45 +00:00
parent eb801df552
commit 52676016fb
40 changed files with 12445 additions and 0 deletions

184
src/pages/AccountsPage.tsx Normal file
View 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>
);
}

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

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