- 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>
185 lines
8.3 KiB
TypeScript
185 lines
8.3 KiB
TypeScript
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>
|
|
);
|
|
}
|