- 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>
383 lines
15 KiB
TypeScript
383 lines
15 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Terminal, ShieldCheck, Radio, History, Mail, Activity, Maximize2, Minimize2,
|
|
Search, Download, Filter, AlertOctagon, GitCompare
|
|
} from 'lucide-react';
|
|
import type { BottomTab, TerminalEntry, AuditEntry, ValidationIssue } from '../types';
|
|
import { sampleTerminal, sampleValidation, sampleAudit, sampleSettlement, sampleReconciliation, sampleExceptions, sampleMessageQueue, sampleEvents } from '../data/sampleData';
|
|
|
|
const tabs: { id: BottomTab; icon: typeof Terminal; label: string }[] = [
|
|
{ id: 'terminal', icon: Terminal, label: 'Terminal' },
|
|
{ id: 'validation', icon: ShieldCheck, label: 'Validation' },
|
|
{ id: '800system', icon: Radio, label: '800 System' },
|
|
{ id: 'settlement', icon: Activity, label: 'Settlement Queue' },
|
|
{ id: 'audit', icon: History, label: 'Audit Trail' },
|
|
{ id: 'messages', icon: Mail, label: 'Messages' },
|
|
{ id: 'events', icon: Activity, label: 'Events' },
|
|
{ id: 'reconciliation', icon: GitCompare, label: 'Reconciliation' },
|
|
{ id: 'exceptions', icon: AlertOctagon, label: 'Exceptions' },
|
|
];
|
|
|
|
interface BottomPanelProps {
|
|
height: number;
|
|
isExpanded: boolean;
|
|
onToggleExpand: () => void;
|
|
terminalEntries: TerminalEntry[];
|
|
auditEntries: AuditEntry[];
|
|
validationIssues: ValidationIssue[];
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
settled: '#22c55e',
|
|
pending: '#eab308',
|
|
in_review: '#3b82f6',
|
|
awaiting_approval: '#f97316',
|
|
dispatched: '#3b82f6',
|
|
partially_settled: '#a855f7',
|
|
failed: '#ef4444',
|
|
};
|
|
|
|
const levelColors: Record<string, string> = {
|
|
info: '#6b7280',
|
|
warn: '#eab308',
|
|
error: '#ef4444',
|
|
success: '#22c55e',
|
|
};
|
|
|
|
export default function BottomPanel({ height, isExpanded, onToggleExpand, terminalEntries, auditEntries, validationIssues }: BottomPanelProps) {
|
|
const [activeTab, setActiveTab] = useState<BottomTab>('terminal');
|
|
const [terminalFilter, setTerminalFilter] = useState('');
|
|
const [bottomSearch, setBottomSearch] = useState('');
|
|
const [showSearchBar, setShowSearchBar] = useState(false);
|
|
const [showFilterBar, setShowFilterBar] = useState(false);
|
|
const [levelFilter, setLevelFilter] = useState<string>('all');
|
|
|
|
const allTerminal = [...sampleTerminal, ...terminalEntries];
|
|
const filteredTerminal = allTerminal.filter(e => {
|
|
const matchesText = e.message.toLowerCase().includes(terminalFilter.toLowerCase()) ||
|
|
e.source.toLowerCase().includes(terminalFilter.toLowerCase());
|
|
const matchesLevel = levelFilter === 'all' || e.level === levelFilter;
|
|
return matchesText && matchesLevel;
|
|
});
|
|
|
|
const allAudit = [...sampleAudit, ...auditEntries];
|
|
const allValidation = validationIssues.length > 0 ? validationIssues : sampleValidation;
|
|
|
|
const handleExport = () => {
|
|
let content = '';
|
|
if (activeTab === 'terminal') {
|
|
content = allTerminal.map(e => `[${e.timestamp.toISOString()}] [${e.level}] [${e.source}] ${e.message}`).join('\n');
|
|
} else if (activeTab === 'audit') {
|
|
content = allAudit.map(e => `[${e.timestamp.toISOString()}] ${e.user} ${e.action}: ${e.detail}`).join('\n');
|
|
} else if (activeTab === 'validation') {
|
|
content = allValidation.map(e => `[${e.severity}] ${e.node ? e.node + ': ' : ''}${e.message}`).join('\n');
|
|
} else {
|
|
content = `Export of ${activeTab} tab data`;
|
|
}
|
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `transactflow-${activeTab}-${new Date().toISOString().slice(0, 10)}.txt`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
return (
|
|
<div className="bottom-panel" style={{ height: isExpanded ? '50vh' : height }}>
|
|
<div className="bottom-panel-header">
|
|
<div className="bottom-panel-tabs">
|
|
{tabs.map(tab => {
|
|
const Icon = tab.icon;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
className={`bottom-tab ${activeTab === tab.id ? 'active' : ''}`}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
>
|
|
<Icon size={13} />
|
|
<span>{tab.label}</span>
|
|
{tab.id === 'validation' && (
|
|
<span className="tab-badge info">{allValidation.length}</span>
|
|
)}
|
|
{tab.id === 'settlement' && (
|
|
<span className="tab-badge warn">{sampleSettlement.filter(s => s.status === 'pending').length}</span>
|
|
)}
|
|
{tab.id === 'exceptions' && (
|
|
<span className="tab-badge error">{sampleExceptions.length}</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="bottom-panel-actions">
|
|
<button className="icon-btn-sm" title="Filter" onClick={() => { setShowFilterBar(!showFilterBar); setShowSearchBar(false); }}>
|
|
<Filter size={13} />
|
|
</button>
|
|
<button className="icon-btn-sm" title="Search" onClick={() => { setShowSearchBar(!showSearchBar); setShowFilterBar(false); }}>
|
|
<Search size={13} />
|
|
</button>
|
|
<button className="icon-btn-sm" title="Export" onClick={handleExport}>
|
|
<Download size={13} />
|
|
</button>
|
|
<button className="icon-btn-sm" title={isExpanded ? 'Minimize' : 'Maximize'} onClick={onToggleExpand}>
|
|
{isExpanded ? <Minimize2 size={13} /> : <Maximize2 size={13} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{showSearchBar && (
|
|
<div className="bottom-search-bar">
|
|
<Search size={12} />
|
|
<input
|
|
type="text"
|
|
placeholder="Search in panel..."
|
|
value={bottomSearch}
|
|
onChange={e => setBottomSearch(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{showFilterBar && activeTab === 'terminal' && (
|
|
<div className="bottom-filter-bar">
|
|
{['all', 'info', 'warn', 'error', 'success'].map(level => (
|
|
<button
|
|
key={level}
|
|
className={`filter-pill ${levelFilter === level ? 'active' : ''}`}
|
|
onClick={() => setLevelFilter(level)}
|
|
>
|
|
{level === 'all' ? 'All' : level.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bottom-panel-content">
|
|
{activeTab === 'terminal' && (
|
|
<div className="terminal-content">
|
|
<div className="terminal-filter">
|
|
<Search size={12} />
|
|
<input
|
|
type="text"
|
|
placeholder="Filter logs..."
|
|
value={terminalFilter}
|
|
onChange={e => setTerminalFilter(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="terminal-entries">
|
|
{filteredTerminal.map(entry => (
|
|
<div key={entry.id} className="terminal-entry">
|
|
<span className="terminal-time">
|
|
{entry.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
</span>
|
|
<span className="terminal-level" style={{ color: levelColors[entry.level] }}>
|
|
[{entry.level.toUpperCase()}]
|
|
</span>
|
|
<span className="terminal-source">[{entry.source}]</span>
|
|
<span className="terminal-msg">{entry.message}</span>
|
|
</div>
|
|
))}
|
|
<div className="terminal-cursor">
|
|
<span className="cursor-blink">▌</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'validation' && (
|
|
<div className="validation-content">
|
|
{allValidation.map(issue => (
|
|
<div key={issue.id} className={`validation-entry ${issue.severity}`}>
|
|
<span className="validation-severity">{issue.severity.toUpperCase()}</span>
|
|
{issue.node && <span className="validation-node">{issue.node}</span>}
|
|
<span className="validation-msg">{issue.message}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === '800system' && (
|
|
<div className="system-800-content">
|
|
<div className="system-800-grid">
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">Message Queue</div>
|
|
<div className="system-800-card-value">0 pending</div>
|
|
<div className="system-800-card-status healthy">Healthy</div>
|
|
</div>
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">Core Banking</div>
|
|
<div className="system-800-card-value">Connected</div>
|
|
<div className="system-800-card-status healthy">Online</div>
|
|
</div>
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">Ledger Feed</div>
|
|
<div className="system-800-card-value">0 postings/s</div>
|
|
<div className="system-800-card-status idle">Idle</div>
|
|
</div>
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">SWIFT Gateway</div>
|
|
<div className="system-800-card-value">Ready</div>
|
|
<div className="system-800-card-status healthy">Online</div>
|
|
</div>
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">ISO-20022 Engine</div>
|
|
<div className="system-800-card-value">3 schemas</div>
|
|
<div className="system-800-card-status healthy">Loaded</div>
|
|
</div>
|
|
<div className="system-800-card">
|
|
<div className="system-800-card-header">Retry Queue</div>
|
|
<div className="system-800-card-value">0 items</div>
|
|
<div className="system-800-card-status healthy">Clear</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'settlement' && (
|
|
<div className="settlement-content">
|
|
<table className="settlement-table">
|
|
<thead>
|
|
<tr>
|
|
<th>TX ID</th>
|
|
<th>Status</th>
|
|
<th>Amount</th>
|
|
<th>Asset</th>
|
|
<th>Counterparty</th>
|
|
<th>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sampleSettlement.map(item => (
|
|
<tr key={item.id}>
|
|
<td className="mono">{item.txId}</td>
|
|
<td>
|
|
<span className="status-badge" style={{ color: statusColors[item.status], borderColor: statusColors[item.status] + '40' }}>
|
|
{item.status.replace(/_/g, ' ')}
|
|
</span>
|
|
</td>
|
|
<td className="mono">{item.amount}</td>
|
|
<td>{item.asset}</td>
|
|
<td>{item.counterparty}</td>
|
|
<td className="mono">{item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'audit' && (
|
|
<div className="audit-content">
|
|
{allAudit.map(entry => (
|
|
<div key={entry.id} className="audit-entry">
|
|
<span className="audit-time">
|
|
{entry.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
</span>
|
|
<span className="audit-user">{entry.user}</span>
|
|
<span className="audit-action">{entry.action}</span>
|
|
<span className="audit-detail">{entry.detail}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'messages' && (
|
|
<div className="messages-content">
|
|
<table className="settlement-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Direction</th>
|
|
<th>Counterparty</th>
|
|
<th>Status</th>
|
|
<th>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sampleMessageQueue.map(msg => (
|
|
<tr key={msg.id}>
|
|
<td className="mono">{msg.msgType}</td>
|
|
<td>
|
|
<span className={`direction-badge ${msg.direction}`}>
|
|
{msg.direction === 'inbound' ? '← IN' : '→ OUT'}
|
|
</span>
|
|
</td>
|
|
<td>{msg.counterparty}</td>
|
|
<td>
|
|
<span className="status-badge" style={{ color: msg.status === 'sent' ? '#22c55e' : msg.status === 'received' ? '#3b82f6' : '#eab308' }}>
|
|
{msg.status}
|
|
</span>
|
|
</td>
|
|
<td className="mono">{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'events' && (
|
|
<div className="events-content">
|
|
{sampleEvents.map(evt => (
|
|
<div key={evt.id} className="audit-entry">
|
|
<span className="audit-time">
|
|
{evt.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
</span>
|
|
<span className="audit-action">{evt.type}</span>
|
|
<span className="audit-detail">{evt.detail}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'reconciliation' && (
|
|
<div className="reconciliation-content">
|
|
<table className="settlement-table">
|
|
<thead>
|
|
<tr>
|
|
<th>TX ID</th>
|
|
<th>Internal Ref</th>
|
|
<th>External Ref</th>
|
|
<th>Status</th>
|
|
<th>Amount</th>
|
|
<th>Asset</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sampleReconciliation.map(item => (
|
|
<tr key={item.id}>
|
|
<td className="mono">{item.txId}</td>
|
|
<td className="mono">{item.internalRef}</td>
|
|
<td className="mono">{item.externalRef}</td>
|
|
<td>
|
|
<span className="status-badge" style={{ color: item.status === 'matched' ? '#22c55e' : '#ef4444' }}>
|
|
{item.status}
|
|
</span>
|
|
</td>
|
|
<td className="mono">{item.amount}</td>
|
|
<td>{item.asset}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'exceptions' && (
|
|
<div className="exceptions-content">
|
|
{sampleExceptions.map(exc => (
|
|
<div key={exc.id} className={`validation-entry ${exc.severity}`}>
|
|
<span className="validation-severity">{exc.severity.toUpperCase()}</span>
|
|
<span className="validation-node">{exc.txId}</span>
|
|
<span className="exception-type">{exc.type}</span>
|
|
<span className="validation-msg">{exc.message}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|