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:
353
src/components/Canvas.tsx
Normal file
353
src/components/Canvas.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import { useCallback, useRef, useState, type DragEvent } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
type Connection,
|
||||
type Node,
|
||||
type Edge,
|
||||
BackgroundVariant,
|
||||
type OnNodesChange,
|
||||
type OnEdgesChange,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import TransactionNodeComponent from './TransactionNode';
|
||||
import {
|
||||
Save, GitBranch, ShieldCheck, FlaskConical, Play,
|
||||
AlertTriangle, CheckCircle2, DollarSign, Clock, Globe,
|
||||
Undo2, Redo2, Copy, Trash2, Plus, X, SplitSquareHorizontal,
|
||||
ZoomIn, ZoomOut, Maximize
|
||||
} from 'lucide-react';
|
||||
import type { ComponentItem, TransactionTab, SessionMode } from '../types';
|
||||
|
||||
const nodeTypes = { transactionNode: TransactionNodeComponent };
|
||||
|
||||
interface CanvasProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
setNodes: (updater: Node[] | ((prev: Node[]) => Node[])) => void;
|
||||
setEdges: (updater: Edge[] | ((prev: Edge[]) => Edge[])) => void;
|
||||
onNodesChange: OnNodesChange;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
onConnect: (params: Connection) => void;
|
||||
onSelectionChange: (params: { nodes: Node[] }) => void;
|
||||
onDropComponent: (item: ComponentItem, position: { x: number; y: number }) => void;
|
||||
onValidate: () => void;
|
||||
onSimulate: () => void;
|
||||
onExecute: () => void;
|
||||
transactionName: string;
|
||||
onRenameTransaction: (name: string) => void;
|
||||
isSimulating: boolean;
|
||||
simulationResults: string | null;
|
||||
onDismissSimulation: () => void;
|
||||
mode: SessionMode;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
selectedNodeIds: Set<string>;
|
||||
onDeleteSelected: () => void;
|
||||
onDuplicateSelected: () => void;
|
||||
transactionTabs: TransactionTab[];
|
||||
activeTransactionId: string;
|
||||
onSwitchTab: (id: string) => void;
|
||||
onAddTab: () => void;
|
||||
onCloseTab: (id: string) => void;
|
||||
splitView: boolean;
|
||||
onToggleSplitView: () => void;
|
||||
pushHistory: (nodes: Node[], edges: Edge[]) => void;
|
||||
}
|
||||
|
||||
function CanvasInner({
|
||||
nodes, edges,
|
||||
onNodesChange, onEdgesChange, onConnect, onSelectionChange, onDropComponent,
|
||||
onValidate, onSimulate, onExecute,
|
||||
transactionName, onRenameTransaction,
|
||||
isSimulating, simulationResults, onDismissSimulation,
|
||||
mode, canUndo, canRedo, onUndo, onRedo,
|
||||
selectedNodeIds, onDeleteSelected, onDuplicateSelected,
|
||||
transactionTabs, activeTransactionId, onSwitchTab, onAddTab, onCloseTab,
|
||||
splitView, onToggleSplitView,
|
||||
}: CanvasProps) {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editName, setEditName] = useState(transactionName);
|
||||
const [zoomLevel, setZoomLevel] = useState(100);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
|
||||
const onDragOver = useCallback((event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
const data = event.dataTransfer.getData('application/transactflow-component');
|
||||
if (!data) return;
|
||||
const item: ComponentItem = JSON.parse(data);
|
||||
const wrapperBounds = reactFlowWrapper.current?.getBoundingClientRect();
|
||||
if (!wrapperBounds) return;
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
onDropComponent(item, position);
|
||||
},
|
||||
[onDropComponent, reactFlowInstance]
|
||||
);
|
||||
|
||||
const onMoveEnd = useCallback(() => {
|
||||
const zoom = reactFlowInstance.getZoom();
|
||||
setZoomLevel(Math.round(zoom * 100));
|
||||
}, [reactFlowInstance]);
|
||||
|
||||
const handleZoomIn = () => { reactFlowInstance.zoomIn(); setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)); };
|
||||
const handleZoomOut = () => { reactFlowInstance.zoomOut(); setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)); };
|
||||
const handleFitView = () => { reactFlowInstance.fitView(); setTimeout(() => setZoomLevel(Math.round(reactFlowInstance.getZoom() * 100)), 100); };
|
||||
|
||||
const errorCount = nodes.filter(n => (n.data as Record<string, unknown>).status === 'error').length;
|
||||
const warningCount = nodes.filter(n => (n.data as Record<string, unknown>).status === 'warning').length;
|
||||
|
||||
const commitName = () => {
|
||||
setIsEditingName(false);
|
||||
if (editName.trim()) onRenameTransaction(editName.trim());
|
||||
else setEditName(transactionName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="canvas-container">
|
||||
{/* Transaction tabs */}
|
||||
<div className="transaction-tabs">
|
||||
{transactionTabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`transaction-tab ${tab.id === activeTransactionId ? 'active' : ''}`}
|
||||
onClick={() => onSwitchTab(tab.id)}
|
||||
>
|
||||
<span>{tab.name}</span>
|
||||
{transactionTabs.length > 1 && (
|
||||
<button className="tab-close" onClick={e => { e.stopPropagation(); onCloseTab(tab.id); }}>
|
||||
<X size={10} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button className="transaction-tab-add" onClick={onAddTab} title="New Transaction">
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="canvas-header">
|
||||
<div className="canvas-header-left">
|
||||
{isEditingName ? (
|
||||
<input
|
||||
className="canvas-tx-name-input"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
onBlur={commitName}
|
||||
onKeyDown={e => { if (e.key === 'Enter') commitName(); if (e.key === 'Escape') { setEditName(transactionName); setIsEditingName(false); } }}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="canvas-tx-name"
|
||||
onClick={() => { setIsEditingName(true); setEditName(transactionName); }}
|
||||
title="Click to rename"
|
||||
>
|
||||
{transactionName}
|
||||
</span>
|
||||
)}
|
||||
<span className="canvas-version">v1.0</span>
|
||||
<span className="canvas-save-state">
|
||||
<Save size={12} /> Saved
|
||||
</span>
|
||||
<div className="canvas-toolbar-separator" />
|
||||
<button className="canvas-toolbar-btn" onClick={onUndo} disabled={!canUndo} title="Undo (Ctrl+Z)">
|
||||
<Undo2 size={14} />
|
||||
</button>
|
||||
<button className="canvas-toolbar-btn" onClick={onRedo} disabled={!canRedo} title="Redo (Ctrl+Y)">
|
||||
<Redo2 size={14} />
|
||||
</button>
|
||||
<div className="canvas-toolbar-separator" />
|
||||
<button className="canvas-toolbar-btn" onClick={onDuplicateSelected} disabled={selectedNodeIds.size === 0} title="Duplicate (Ctrl+D)">
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
<button className="canvas-toolbar-btn" onClick={onDeleteSelected} disabled={selectedNodeIds.size === 0} title="Delete (Del)">
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
<div className="canvas-toolbar-separator" />
|
||||
<button className="canvas-toolbar-btn" onClick={onToggleSplitView} title="Split View">
|
||||
<SplitSquareHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="canvas-header-center">
|
||||
<button className="canvas-env-btn">
|
||||
<GitBranch size={13} />
|
||||
<span>{mode}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="canvas-header-right">
|
||||
<button className="canvas-action-btn validate" onClick={onValidate}>
|
||||
<ShieldCheck size={14} /> Validate
|
||||
</button>
|
||||
<button className="canvas-action-btn simulate" onClick={onSimulate} disabled={isSimulating}>
|
||||
<FlaskConical size={14} /> {isSimulating ? 'Simulating...' : 'Simulate'}
|
||||
</button>
|
||||
<button className="canvas-action-btn execute" onClick={onExecute}>
|
||||
<Play size={14} /> Execute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="canvas-body" ref={reactFlowWrapper}>
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%' }}>
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onMoveEnd={onMoveEnd}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
snapToGrid
|
||||
snapGrid={[16, 16]}
|
||||
multiSelectionKeyCode="Shift"
|
||||
deleteKeyCode={null}
|
||||
defaultEdgeOptions={{
|
||||
animated: true,
|
||||
style: { stroke: '#3b82f6', strokeWidth: 2 },
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#333" />
|
||||
<Controls className="canvas-controls" showZoom={false} showFitView={false} showInteractive={false}>
|
||||
<button className="react-flow__controls-button" onClick={handleZoomIn} title="Zoom In">
|
||||
<ZoomIn size={14} />
|
||||
</button>
|
||||
<button className="react-flow__controls-button" onClick={handleZoomOut} title="Zoom Out">
|
||||
<ZoomOut size={14} />
|
||||
</button>
|
||||
<button className="react-flow__controls-button zoom-display" title="Current Zoom">
|
||||
{zoomLevel}%
|
||||
</button>
|
||||
<button className="react-flow__controls-button" onClick={handleFitView} title="Fit View">
|
||||
<Maximize size={14} />
|
||||
</button>
|
||||
</Controls>
|
||||
<MiniMap
|
||||
className="canvas-minimap"
|
||||
nodeColor={(n) => {
|
||||
const d = n.data as Record<string, unknown>;
|
||||
return (d?.color as string) || '#3b82f6';
|
||||
}}
|
||||
maskColor="rgba(0,0,0,0.7)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
{splitView && (
|
||||
<>
|
||||
<div style={{ width: 1, background: '#2a2a32', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, position: 'relative', background: '#0e0e10', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center', color: '#5c5c68' }}>
|
||||
<SplitSquareHorizontal size={32} style={{ marginBottom: 8, opacity: 0.4 }} />
|
||||
<p style={{ fontSize: 13 }}>Comparison View</p>
|
||||
<p style={{ fontSize: 11, marginTop: 4 }}>Select a saved version or branch to compare</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodes.length === 0 && !splitView && (
|
||||
<div className="canvas-empty">
|
||||
<div className="canvas-empty-content">
|
||||
<div className="canvas-empty-icon">⚡</div>
|
||||
<h3>Start Building</h3>
|
||||
<p>Drag components from the left panel onto the canvas to compose your transaction flow</p>
|
||||
<p className="canvas-empty-hint">or press <kbd>Ctrl+K</kbd> to search components</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simulation overlay */}
|
||||
{isSimulating && (
|
||||
<div className="simulation-overlay">
|
||||
<div className="simulation-spinner" />
|
||||
<span>Running simulation...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{simulationResults && (
|
||||
<div className="simulation-results-overlay">
|
||||
<div className="simulation-results-card">
|
||||
<div className="simulation-results-header">
|
||||
<CheckCircle2 size={16} color="#22c55e" />
|
||||
<span>Simulation Results</span>
|
||||
<button className="simulation-dismiss" onClick={onDismissSimulation}><X size={14} /></button>
|
||||
</div>
|
||||
<pre className="simulation-results-body">{simulationResults}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="canvas-inspector">
|
||||
<div className="inspector-item">
|
||||
<CheckCircle2 size={12} color="#22c55e" />
|
||||
<span>{nodes.length} nodes</span>
|
||||
</div>
|
||||
<div className="inspector-item">
|
||||
<span>{edges.length} connections</span>
|
||||
</div>
|
||||
<div className="inspector-separator" />
|
||||
<div className="inspector-item">
|
||||
<AlertTriangle size={12} color={errorCount > 0 ? '#ef4444' : '#555'} />
|
||||
<span>{errorCount} errors</span>
|
||||
</div>
|
||||
<div className="inspector-item">
|
||||
<AlertTriangle size={12} color={warningCount > 0 ? '#eab308' : '#555'} />
|
||||
<span>{warningCount} warnings</span>
|
||||
</div>
|
||||
<div className="inspector-separator" />
|
||||
<div className="inspector-item">
|
||||
<DollarSign size={12} />
|
||||
<span>Est. fees: {nodes.length > 0 ? '$0.02%' : '—'}</span>
|
||||
</div>
|
||||
<div className="inspector-item">
|
||||
<Clock size={12} />
|
||||
<span>Settlement: {nodes.length > 0 ? 'T+1' : '—'}</span>
|
||||
</div>
|
||||
<div className="inspector-item">
|
||||
<Globe size={12} />
|
||||
<span>Jurisdictions: {nodes.length > 0 ? 'Multi' : '—'}</span>
|
||||
</div>
|
||||
{selectedNodeIds.size > 0 && (
|
||||
<>
|
||||
<div className="inspector-separator" />
|
||||
<div className="inspector-item selected-info">
|
||||
<span>{selectedNodeIds.size} selected</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Canvas(props: CanvasProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<CanvasInner {...props} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user