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:
382
src/components/BottomPanel.tsx
Normal file
382
src/components/BottomPanel.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user