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

View File

@@ -0,0 +1,63 @@
# Testing TransactFlow IDE
## Overview
TransactFlow is a React 18 + TypeScript + Vite app using @xyflow/react for a drag-and-drop graph editor. It deploys as a static frontend site (no backend).
## Deployed URL
https://dist-dgoompqy.devinapps.com
## Local Dev
```bash
cd /home/ubuntu/repos/transaction-builder
npm install
npm run dev # starts on localhost:5174
```
## Testing Approach
### Tool Selection
- **Browser tool**: Use for most UI interactions (clicking, typing, verifying DOM state via console). Works well for buttons, inputs, tabs, dropdowns.
- **Playwright CDP**: Required for drag-and-drop testing. React Flow's drag-and-drop uses native browser events that synthetic DOM events cannot replicate. Connect via `chromium.connectOverCDP('http://127.0.0.1:<port>')`. The Chrome CDP port may be ephemeral — find it with `ss -tlnp 2>/dev/null | grep chrome`.
- **Browser console**: Use `document.querySelector`/`querySelectorAll` for DOM assertions. Returns exact counts and text content for verification.
### Key Patterns
#### React Controlled Inputs
React controlled inputs (`<input value={state} onChange={...}>`) cannot be cleared via `element.value = ''` + synthetic events. React manages the value internally. **Workaround**: Reload the page to reset state rather than fighting React's control.
#### devinid Drift After DOM Changes
The browser tool assigns `devinid` attributes based on current DOM state. After significant DOM changes (collapsing accordions, opening modals, switching tabs), cached devinids become invalid. **Workaround**: Re-query the HTML or reload the page after major DOM mutations to get fresh devinids.
#### Keyboard Shortcuts (Ctrl+K, Ctrl+B, etc.)
Browser automation tools may intercept `Ctrl+K` before it reaches the page. Synthetic `KeyboardEvent` dispatch on `window` also might not trigger React's state updates reliably. **Workaround**: Use the UI button that triggers the same action (e.g., Command Palette icon button instead of Ctrl+K).
#### Command Palette
The command palette overlay renders as `.command-palette` with input `.command-palette-input` and results `.command-palette-results`. Commands are `.command-item` elements with `.command-label` text. The palette input might not have a visible devinid in truncated HTML — search the full page HTML file for `placeholder="Type a command"`.
### Component Architecture (for test assertions)
- **TitleBar**: Mode selector (`.mode-selector` / `.mode-dropdown` / `.mode-option`), search bar, validate/simulate/execute buttons
- **ActivityBar**: 10 `.activity-btn` icon buttons
- **LeftPanel**: Search input, filter buttons (All/Favorites/Recent), `.component-category` with `.category-header` and `.category-items`, `.component-item` elements
- **Canvas**: React Flow container, `.canvas-empty-content` (shown when `nodes.length === 0`), `.canvas-inspector` with node/connection counts
- **RightPanel**: `.chat-header-agent` shows active agent name, `.agent-tab` buttons for switching, `.chat-message.user` and `.chat-message.agent` for messages, agent responses have 800ms delay
- **BottomPanel**: `.bottom-tab` buttons, `.system-800-card` (6 cards), `.settlement-table` (4 rows), `.audit-content` with `.audit-entry` elements
- **CommandPalette**: `.command-palette-overlay`, `.command-palette`, `.command-item`, `.command-label`
### Test Data (hardcoded in source)
- Default favorites: Transfer, Swap, KYC (Set in LeftPanel.tsx)
- 7 component categories, 56 total components
- 7 agent types: Builder, Compliance, Routing, ISO-20022, Settlement, Risk, Documentation
- 6 system status cards in 800 System tab
- 4 settlement queue rows (includes TX-2024-0847)
- Audit trail contains SESSION_START and ENGINE_LOAD events
- 4 session modes: Sandbox, Simulate, Live, Compliance Review
- Builder agent responds with "Transfer" (when input doesn't contain "swap") or "Swap" (when it does)
- Compliance agent responds with "No policy violations"
## Devin Secrets Needed
None — this is a purely frontend static site with no auth required.
## Common Issues
- If Chrome CDP port is not 29229, the browser was launched with `--remote-debugging-port=0` (random). Restart browser via `browser(action="restart")` and find the new port.
- React Flow nodes require native browser drag events. Use Playwright's `dragTo()` method, not synthetic `DragEvent` dispatch.
- After page reload, all React state resets (nodes cleared, chat history cleared, filters reset to defaults). Plan test sequences accordingly.

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Solace Bank Group PLC — Treasury Management Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3433
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "transaction-builder",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@xyflow/react": "^12.10.2",
"ethers": "^6.16.0",
"lucide-react": "^1.8.0",
"playwright": "^1.59.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.4"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

583
src/App.tsx Normal file
View File

@@ -0,0 +1,583 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { addEdge, applyNodeChanges, applyEdgeChanges, type Node, type Edge, type Connection, type NodeChange, type EdgeChange } from '@xyflow/react';
import TitleBar from './components/TitleBar';
import ActivityBar from './components/ActivityBar';
import LeftPanel from './components/LeftPanel';
import Canvas from './components/Canvas';
import RightPanel from './components/RightPanel';
import BottomPanel from './components/BottomPanel';
import CommandPalette from './components/CommandPalette';
import type { ActivityTab, SessionMode, ComponentItem, HistoryEntry, TransactionTab, TerminalEntry, AuditEntry, ValidationIssue } from './types';
const STORAGE_KEY = 'transactflow-workspace';
function loadWorkspace() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch { /* ignore */ }
return null;
}
function saveWorkspace(state: Record<string, unknown>) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch { /* ignore */ }
}
export default function App() {
const saved = useRef(loadWorkspace());
const [activityTab, setActivityTab] = useState<ActivityTab>(saved.current?.activityTab || 'builder');
const [leftOpen, setLeftOpen] = useState(saved.current?.leftOpen ?? true);
const [rightOpen, setRightOpen] = useState(saved.current?.rightOpen ?? true);
const [bottomOpen, setBottomOpen] = useState(saved.current?.bottomOpen ?? true);
const [bottomExpanded, setBottomExpanded] = useState(false);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
const [mode, setMode] = useState<SessionMode>(saved.current?.mode || 'Sandbox');
const [leftWidth, setLeftWidth] = useState(saved.current?.leftWidth ?? 280);
const [rightWidth, setRightWidth] = useState(saved.current?.rightWidth ?? 320);
const [bottomHeight, setBottomHeight] = useState(saved.current?.bottomHeight ?? 220);
// Transaction tabs
const [transactionTabs, setTransactionTabs] = useState<TransactionTab[]>([
{ id: 'tx-1', name: 'Untitled Transaction', nodes: [], edges: [] },
]);
const [activeTransactionId, setActiveTransactionId] = useState('tx-1');
const activeTransaction = transactionTabs.find(t => t.id === activeTransactionId)!;
const nodes = activeTransaction.nodes;
const edges = activeTransaction.edges;
const setNodes = useCallback((updater: Node[] | ((prev: Node[]) => Node[])) => {
setTransactionTabs(prev => prev.map(t => {
if (t.id !== activeTransactionId) return t;
const newNodes = typeof updater === 'function' ? updater(t.nodes) : updater;
return { ...t, nodes: newNodes };
}));
}, [activeTransactionId]);
const setEdges = useCallback((updater: Edge[] | ((prev: Edge[]) => Edge[])) => {
setTransactionTabs(prev => prev.map(t => {
if (t.id !== activeTransactionId) return t;
const newEdges = typeof updater === 'function' ? updater(t.edges) : updater;
return { ...t, edges: newEdges };
}));
}, [activeTransactionId]);
// Undo/redo
const [history, setHistory] = useState<HistoryEntry[]>([{ nodes: [], edges: [] }]);
const [historyIndex, setHistoryIndex] = useState(0);
const skipHistoryRef = useRef(false);
const pushHistory = useCallback((n: Node[], e: Edge[]) => {
if (skipHistoryRef.current) { skipHistoryRef.current = false; return; }
setHistory(prev => {
const trimmed = prev.slice(0, historyIndex + 1);
const entry = { nodes: JSON.parse(JSON.stringify(n)), edges: JSON.parse(JSON.stringify(e)) };
const next = [...trimmed, entry];
if (next.length > 50) next.shift();
return next;
});
setHistoryIndex(prev => Math.min(prev + 1, 50));
}, [historyIndex]);
const undo = useCallback(() => {
if (historyIndex <= 0) return;
const newIndex = historyIndex - 1;
const entry = history[newIndex];
if (!entry) return;
skipHistoryRef.current = true;
setHistoryIndex(newIndex);
setNodes(JSON.parse(JSON.stringify(entry.nodes)));
setEdges(JSON.parse(JSON.stringify(entry.edges)));
}, [historyIndex, history, setNodes, setEdges]);
const redo = useCallback(() => {
if (historyIndex >= history.length - 1) return;
const newIndex = historyIndex + 1;
const entry = history[newIndex];
if (!entry) return;
skipHistoryRef.current = true;
setHistoryIndex(newIndex);
setNodes(JSON.parse(JSON.stringify(entry.nodes)));
setEdges(JSON.parse(JSON.stringify(entry.edges)));
}, [historyIndex, history, setNodes, setEdges]);
// Selected nodes
const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
const selectedNodes = nodes.filter(n => selectedNodeIds.has(n.id));
// Recent components
const [recentComponents, setRecentComponents] = useState<string[]>([]);
const addRecentComponent = useCallback((id: string) => {
setRecentComponents(prev => {
const next = [id, ...prev.filter(x => x !== id)];
return next.slice(0, 20);
});
}, []);
// Terminal log entries (live)
const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
const addTerminalEntry = useCallback((level: TerminalEntry['level'], source: string, message: string) => {
setTerminalEntries(prev => [...prev, {
id: Date.now().toString(),
timestamp: new Date(),
level,
source,
message,
}]);
}, []);
// Audit entries (live)
const [auditEntries, setAuditEntries] = useState<AuditEntry[]>([]);
const addAuditEntry = useCallback((action: string, detail: string) => {
setAuditEntries(prev => [...prev, {
id: Date.now().toString(),
timestamp: new Date(),
user: 'user',
action,
detail,
}]);
}, []);
// Validation state
const [validationIssues, setValidationIssues] = useState<ValidationIssue[]>([]);
const [isSimulating, setIsSimulating] = useState(false);
const [simulationResults, setSimulationResults] = useState<string | null>(null);
// Split view
const [splitView, setSplitView] = useState(false);
// Resizable panels
const resizing = useRef<{ side: 'left' | 'right' | 'bottom'; startPos: number; startSize: number } | null>(null);
useEffect(() => {
const onMouseMove = (e: MouseEvent) => {
if (!resizing.current) return;
const { side, startPos, startSize } = resizing.current;
if (side === 'left') {
const delta = e.clientX - startPos;
setLeftWidth(Math.max(200, Math.min(500, startSize + delta)));
} else if (side === 'right') {
const delta = startPos - e.clientX;
setRightWidth(Math.max(240, Math.min(600, startSize + delta)));
} else if (side === 'bottom') {
const delta = startPos - e.clientY;
setBottomHeight(Math.max(120, Math.min(500, startSize + delta)));
}
};
const onMouseUp = () => { resizing.current = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; };
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); };
}, []);
const startResize = useCallback((side: 'left' | 'right' | 'bottom', e: React.MouseEvent) => {
resizing.current = {
side,
startPos: side === 'bottom' ? e.clientY : e.clientX,
startSize: side === 'left' ? leftWidth : side === 'right' ? rightWidth : bottomHeight,
};
document.body.style.cursor = side === 'bottom' ? 'row-resize' : 'col-resize';
document.body.style.userSelect = 'none';
}, [leftWidth, rightWidth, bottomHeight]);
// Persist workspace
useEffect(() => {
saveWorkspace({ leftOpen, rightOpen, bottomOpen, leftWidth, rightWidth, bottomHeight, mode, activityTab });
}, [leftOpen, rightOpen, bottomOpen, leftWidth, rightWidth, bottomHeight, mode, activityTab]);
// Toggles
const toggleLeft = useCallback(() => setLeftOpen((p: boolean) => !p), []);
const toggleRight = useCallback(() => setRightOpen((p: boolean) => !p), []);
const toggleBottom = useCallback(() => setBottomOpen((p: boolean) => !p), []);
const toggleCommandPalette = useCallback(() => setCommandPaletteOpen((p: boolean) => !p), []);
// Node operations
const nodeIdCounter = useRef(0);
const onDropComponent = useCallback((item: ComponentItem, position: { x: number; y: number }) => {
const newNode: Node = {
id: `node_${nodeIdCounter.current++}`,
type: 'transactionNode',
position,
data: { label: item.label, category: item.category, icon: item.icon, color: item.color, status: undefined },
};
setNodes(prev => {
const next = [...prev, newNode];
pushHistory(next, edges);
return next;
});
addRecentComponent(item.id);
addTerminalEntry('info', 'canvas', `Node added: ${item.label}`);
addAuditEntry('NODE_ADD', `Added ${item.label} node to canvas`);
}, [setNodes, edges, pushHistory, addRecentComponent, addTerminalEntry, addAuditEntry]);
const onConnect = useCallback((params: Connection) => {
setEdges(prev => {
const next = addEdge({ ...params, animated: true, style: { stroke: '#3b82f6', strokeWidth: 2 } }, prev);
pushHistory(nodes, next);
return next;
});
addTerminalEntry('info', 'canvas', `Edge connected: ${params.source}${params.target}`);
addAuditEntry('EDGE_CREATE', `Connection created`);
}, [setEdges, nodes, pushHistory, addTerminalEntry, addAuditEntry]);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((prev: Node[]) => applyNodeChanges(changes, prev));
}, [setNodes]);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((prev: Edge[]) => applyEdgeChanges(changes, prev));
}, [setEdges]);
const onSelectionChange = useCallback(({ nodes: selectedNodes }: { nodes: Node[] }) => {
setSelectedNodeIds(new Set(selectedNodes.map(n => n.id)));
}, []);
const deleteSelectedNodes = useCallback(() => {
if (selectedNodeIds.size === 0) return;
setNodes(prev => {
const next = prev.filter(n => !selectedNodeIds.has(n.id));
pushHistory(next, edges.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)));
return next;
});
setEdges(prev => prev.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)));
addTerminalEntry('info', 'canvas', `Deleted ${selectedNodeIds.size} node(s)`);
addAuditEntry('NODE_DELETE', `Removed ${selectedNodeIds.size} node(s)`);
setSelectedNodeIds(new Set());
}, [selectedNodeIds, setNodes, setEdges, edges, pushHistory, addTerminalEntry, addAuditEntry]);
const duplicateSelectedNodes = useCallback(() => {
if (selectedNodeIds.size === 0) return;
const selected = nodes.filter(n => selectedNodeIds.has(n.id));
const newNodes = selected.map(n => ({
...n,
id: `node_${nodeIdCounter.current++}`,
position: { x: n.position.x + 40, y: n.position.y + 40 },
selected: false,
}));
setNodes(prev => {
const next = [...prev, ...newNodes];
pushHistory(next, edges);
return next;
});
addTerminalEntry('info', 'canvas', `Duplicated ${selected.length} node(s)`);
addAuditEntry('NODE_DUPLICATE', `Duplicated ${selected.length} node(s)`);
}, [selectedNodeIds, nodes, setNodes, edges, pushHistory, addTerminalEntry, addAuditEntry]);
// Validate
const runValidation = useCallback(() => {
const issues: ValidationIssue[] = [];
if (nodes.length === 0) {
issues.push({ id: 'v1', severity: 'info', message: 'Graph is empty. Add components to begin validation.' });
} else {
const disconnected = nodes.filter(n => !edges.some(e => e.source === n.id || e.target === n.id));
disconnected.forEach(n => {
const d = n.data as Record<string, unknown>;
issues.push({ id: `v-disc-${n.id}`, severity: 'warning', node: (d.label as string) || n.id, message: 'Node is not connected to any other node' });
});
const hasCompliance = nodes.some(n => (n.data as Record<string, unknown>).category === 'compliance');
if (!hasCompliance) {
issues.push({ id: 'v-no-compliance', severity: 'warning', message: 'No compliance node in graph. Consider adding KYC/AML checks.' });
}
const sources = nodes.filter(n => !edges.some(e => e.target === n.id));
const sinks = nodes.filter(n => !edges.some(e => e.source === n.id));
if (sources.length === 0) issues.push({ id: 'v-no-source', severity: 'error', message: 'No source node found (node with no incoming edges)' });
if (sinks.length === 0) issues.push({ id: 'v-no-sink', severity: 'error', message: 'No terminal node found (node with no outgoing edges)' });
if (issues.filter(i => i.severity === 'error').length === 0) {
issues.push({ id: 'v-ok', severity: 'info', message: `Validation passed. ${nodes.length} nodes, ${edges.length} connections verified.` });
}
}
setValidationIssues(issues);
const errs = issues.filter(i => i.severity === 'error').length;
const warns = issues.filter(i => i.severity === 'warning').length;
addTerminalEntry(errs > 0 ? 'error' : warns > 0 ? 'warn' : 'success', 'validation', `Validation complete: ${errs} errors, ${warns} warnings`);
addAuditEntry('VALIDATION_RUN', `Validation: ${errs} errors, ${warns} warnings`);
// Update node statuses
setNodes(prev => prev.map(n => {
const nodeIssues = issues.filter(i => i.node === ((n.data as Record<string, unknown>).label as string));
const hasError = nodeIssues.some(i => i.severity === 'error');
const hasWarning = nodeIssues.some(i => i.severity === 'warning');
return {
...n,
data: { ...n.data, status: hasError ? 'error' : hasWarning ? 'warning' : (nodes.length > 0 ? 'valid' : undefined) },
};
}));
return issues;
}, [nodes, edges, addTerminalEntry, addAuditEntry, setNodes]);
// Simulate
const runSimulation = useCallback(() => {
if (nodes.length === 0) {
addTerminalEntry('warn', 'simulation', 'Cannot simulate empty graph');
return;
}
setIsSimulating(true);
addTerminalEntry('info', 'simulation', 'Starting transaction simulation...');
addAuditEntry('SIMULATION_START', 'Simulation initiated');
setTimeout(() => {
const hasCompliance = nodes.some(n => (n.data as Record<string, unknown>).category === 'compliance');
const routingNodes = nodes.filter(n => (n.data as Record<string, unknown>).category === 'routing');
const fee = (Math.random() * 0.1).toFixed(4);
const results = [
`Simulation complete for ${nodes.length} nodes, ${edges.length} edges`,
`Estimated fees: $${fee}%`,
`Settlement window: T+${routingNodes.length > 0 ? '1' : '2'}`,
`Compliance: ${hasCompliance ? 'All checks passed' : 'WARNING - No compliance checks in flow'}`,
`Routing: ${routingNodes.length} venue(s) evaluated`,
`Status: ${hasCompliance ? 'READY FOR EXECUTION' : 'REVIEW REQUIRED'}`,
].join('\n');
setSimulationResults(results);
setIsSimulating(false);
addTerminalEntry('success', 'simulation', 'Simulation completed successfully');
addAuditEntry('SIMULATION_COMPLETE', `Simulation: ${nodes.length} nodes processed`);
}, 1500);
}, [nodes, edges, addTerminalEntry, addAuditEntry]);
// Execute
const runExecution = useCallback(() => {
if (nodes.length === 0) {
addTerminalEntry('warn', 'execution', 'Cannot execute empty graph');
return;
}
if (mode !== 'Live') {
addTerminalEntry('info', 'execution', `Transaction submitted in ${mode} mode`);
} else {
addTerminalEntry('warn', 'execution', 'Live execution initiated — awaiting confirmation');
}
addAuditEntry('EXECUTE', `Transaction submitted in ${mode} mode`);
addTerminalEntry('success', 'execution', `Transaction ${activeTransaction.name} dispatched to settlement queue`);
}, [nodes, mode, activeTransaction.name, addTerminalEntry, addAuditEntry]);
// Transaction tab management
const addTransactionTab = useCallback(() => {
const id = `tx-${Date.now()}`;
setTransactionTabs(prev => [...prev, { id, name: `Transaction ${prev.length + 1}`, nodes: [], edges: [] }]);
setActiveTransactionId(id);
setHistory([{ nodes: [], edges: [] }]);
setHistoryIndex(0);
addTerminalEntry('info', 'system', 'New transaction tab created');
}, [addTerminalEntry]);
const closeTransactionTab = useCallback((id: string) => {
if (transactionTabs.length <= 1) return;
setTransactionTabs(prev => prev.filter(t => t.id !== id));
if (activeTransactionId === id) {
const remaining = transactionTabs.filter(t => t.id !== id);
setActiveTransactionId(remaining[0].id);
}
}, [transactionTabs, activeTransactionId]);
const renameTransaction = useCallback((name: string) => {
setTransactionTabs(prev => prev.map(t => t.id === activeTransactionId ? { ...t, name } : t));
}, [activeTransactionId]);
// Keyboard shortcuts
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const ctrl = e.ctrlKey || e.metaKey;
if (ctrl && e.key === 'k') { e.preventDefault(); toggleCommandPalette(); }
if (ctrl && e.key === 'b') { e.preventDefault(); toggleLeft(); }
if (ctrl && e.key === 'j') { e.preventDefault(); toggleRight(); }
if (ctrl && e.key === '`') { e.preventDefault(); toggleBottom(); }
if (ctrl && e.shiftKey && e.key === 'V') { e.preventDefault(); runValidation(); }
if (ctrl && e.shiftKey && e.key === 'S') { e.preventDefault(); runSimulation(); }
if (ctrl && e.shiftKey && e.key === 'E') { e.preventDefault(); runExecution(); }
if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
if (ctrl && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) { e.preventDefault(); redo(); }
if (ctrl && e.key === 'd') { e.preventDefault(); duplicateSelectedNodes(); }
if (e.key === 'Delete' || e.key === 'Backspace') {
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return;
if (selectedNodeIds.size > 0) { e.preventDefault(); deleteSelectedNodes(); }
}
if (e.key === 'Escape' && commandPaletteOpen) { setCommandPaletteOpen(false); }
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [commandPaletteOpen, toggleCommandPalette, toggleLeft, toggleRight, toggleBottom, runValidation, runSimulation, runExecution, undo, redo, duplicateSelectedNodes, deleteSelectedNodes, selectedNodeIds]);
// Focus chat
const chatInputRef = useRef<HTMLInputElement>(null);
const focusChat = useCallback(() => { setRightOpen(true); setTimeout(() => chatInputRef.current?.focus(), 100); }, []);
const focusTerminal = useCallback(() => { setBottomOpen(true); }, []);
return (
<div className="app-shell">
<TitleBar
mode={mode}
onModeChange={setMode}
onToggleCommandPalette={toggleCommandPalette}
onValidate={runValidation}
onSimulate={runSimulation}
onExecute={runExecution}
/>
<div className="app-body">
<ActivityBar
activeTab={activityTab}
onTabChange={setActivityTab}
leftPanelOpen={leftOpen}
onToggleLeftPanel={toggleLeft}
/>
<div className="workspace">
<div className="workspace-upper">
{leftOpen && (
<LeftPanel
width={leftWidth}
activityTab={activityTab}
recentComponents={recentComponents}
/>
)}
{leftOpen && (
<div
className="panel-divider vertical"
onMouseDown={e => startResize('left', e)}
/>
)}
<div className="canvas-region">
<Canvas
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onSelectionChange={onSelectionChange}
onDropComponent={onDropComponent}
onValidate={runValidation}
onSimulate={runSimulation}
onExecute={runExecution}
transactionName={activeTransaction.name}
onRenameTransaction={renameTransaction}
isSimulating={isSimulating}
simulationResults={simulationResults}
onDismissSimulation={() => setSimulationResults(null)}
mode={mode}
canUndo={historyIndex > 0}
canRedo={historyIndex < history.length - 1}
onUndo={undo}
onRedo={redo}
selectedNodeIds={selectedNodeIds}
onDeleteSelected={deleteSelectedNodes}
onDuplicateSelected={duplicateSelectedNodes}
transactionTabs={transactionTabs}
activeTransactionId={activeTransactionId}
onSwitchTab={setActiveTransactionId}
onAddTab={addTransactionTab}
onCloseTab={closeTransactionTab}
splitView={splitView}
onToggleSplitView={() => setSplitView(p => !p)}
pushHistory={pushHistory}
/>
</div>
{rightOpen && (
<div
className="panel-divider vertical"
onMouseDown={e => startResize('right', e)}
/>
)}
{rightOpen && (
<RightPanel
width={rightWidth}
nodes={nodes}
edges={edges}
selectedNodes={selectedNodes}
chatInputRef={chatInputRef}
onInsertBlock={onDropComponent}
onRunValidation={runValidation}
onOptimizeRoute={() => {
addTerminalEntry('info', 'routing', 'Route optimization started...');
setTimeout(() => addTerminalEntry('success', 'routing', 'Optimal route found: Banking Rail → SWIFT Gateway (0.02% fee)'), 800);
}}
onRunCompliance={() => {
addTerminalEntry('info', 'compliance', 'Running compliance pass...');
const hasCompliance = nodes.some(n => (n.data as Record<string, unknown>).category === 'compliance');
setTimeout(() => addTerminalEntry(hasCompliance ? 'success' : 'warn', 'compliance', hasCompliance ? 'All compliance checks passed' : 'No compliance nodes found in graph'), 600);
}}
onGenerateSettlement={() => {
addTerminalEntry('info', 'settlement', 'Generating settlement message...');
setTimeout(() => addTerminalEntry('success', 'settlement', 'Settlement instruction generated: pacs.008 message ready'), 700);
}}
/>
)}
</div>
{bottomOpen && (
<div
className="panel-divider horizontal"
onMouseDown={e => startResize('bottom', e)}
/>
)}
{bottomOpen && (
<BottomPanel
height={bottomHeight}
isExpanded={bottomExpanded}
onToggleExpand={() => setBottomExpanded(p => !p)}
terminalEntries={terminalEntries}
auditEntries={auditEntries}
validationIssues={validationIssues}
/>
)}
</div>
</div>
<div className="status-bar">
<div className="status-bar-left">
<span className="status-item">
<span className="status-dot green" /> Connected
</span>
<span className="status-item">{mode}</span>
<span className="status-item">Multi-Jurisdiction</span>
</div>
<div className="status-bar-right">
<span className="status-item">Compliance: Active</span>
<span className="status-item">ISO-20022: Ready</span>
<span className="status-item">Routing: 12 venues</span>
<span className="status-item">
<kbd className="status-kbd">Ctrl+K</kbd> Command Palette
</span>
</div>
</div>
<CommandPalette
isOpen={commandPaletteOpen}
onClose={() => setCommandPaletteOpen(false)}
onToggleLeft={toggleLeft}
onToggleRight={toggleRight}
onToggleBottom={toggleBottom}
onValidate={runValidation}
onSimulate={runSimulation}
onExecute={runExecution}
onNewTransaction={addTransactionTab}
onFocusChat={focusChat}
onFocusTerminal={focusTerminal}
onRunCompliance={() => {
addTerminalEntry('info', 'compliance', 'Running compliance pass...');
setTimeout(() => addTerminalEntry('success', 'compliance', 'Compliance pass completed'), 600);
}}
onOptimizeRoute={() => {
addTerminalEntry('info', 'routing', 'Optimizing routes...');
setTimeout(() => addTerminalEntry('success', 'routing', 'Routes optimized'), 600);
}}
onGenerateISO={() => {
addTerminalEntry('info', 'iso20022', 'Generating ISO-20022 message...');
setTimeout(() => addTerminalEntry('success', 'iso20022', 'pain.001 message generated'), 700);
}}
onExportAudit={() => {
addTerminalEntry('info', 'audit', 'Exporting audit summary...');
setTimeout(() => addTerminalEntry('success', 'audit', 'Audit summary exported'), 500);
}}
onSearchComponents={() => { setLeftOpen(true); setActivityTab('builder'); }}
/>
</div>
);
}

243
src/Portal.tsx Normal file
View File

@@ -0,0 +1,243 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage';
import AccountsPage from './pages/AccountsPage';
import TreasuryPage from './pages/TreasuryPage';
import ReportingPage from './pages/ReportingPage';
import CompliancePage from './pages/CompliancePage';
import SettlementsPage from './pages/SettlementsPage';
import PortalLayout from './components/portal/PortalLayout';
import App from './App';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="portal-loading">
<div className="portal-loading-spinner" />
<span>Initializing secure session...</span>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
export default function Portal() {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="portal-loading">
<div className="portal-loading-spinner" />
<span>Initializing secure session...</span>
</div>
);
}
return (
<Routes>
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/dashboard" replace /> : <LoginPage />}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<PortalLayout>
<DashboardPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/transaction-builder"
element={
<ProtectedRoute>
<PortalLayout>
<div className="transaction-builder-module">
<App />
</div>
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/accounts"
element={
<ProtectedRoute>
<PortalLayout>
<AccountsPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/treasury"
element={
<ProtectedRoute>
<PortalLayout>
<TreasuryPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/reporting"
element={
<ProtectedRoute>
<PortalLayout>
<ReportingPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/compliance"
element={
<ProtectedRoute>
<PortalLayout>
<CompliancePage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/settlements"
element={
<ProtectedRoute>
<PortalLayout>
<SettlementsPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<PortalLayout>
<SettingsPage />
</PortalLayout>
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to={isAuthenticated ? '/dashboard' : '/login'} replace />} />
</Routes>
);
}
function SettingsPage() {
const { user, wallet } = useAuth();
return (
<div className="settings-page">
<div className="page-header">
<h1>Settings</h1>
<p className="page-subtitle">Portal configuration and user preferences</p>
</div>
<div className="settings-grid">
<div className="dashboard-card">
<div className="card-header"><h3>Profile</h3></div>
<div className="settings-section">
<div className="setting-row">
<span className="setting-label">Display Name</span>
<span className="setting-value">{user?.displayName || '—'}</span>
</div>
<div className="setting-row">
<span className="setting-label">Role</span>
<span className="setting-value">{user?.role?.replace('_', ' ') || '—'}</span>
</div>
<div className="setting-row">
<span className="setting-label">Institution</span>
<span className="setting-value">{user?.institution || '—'}</span>
</div>
<div className="setting-row">
<span className="setting-label">Department</span>
<span className="setting-value">{user?.department || '—'}</span>
</div>
<div className="setting-row">
<span className="setting-label">Wallet Address</span>
<span className="setting-value mono">{wallet?.address || '—'}</span>
</div>
<div className="setting-row">
<span className="setting-label">Chain ID</span>
<span className="setting-value">{wallet?.chainId || '—'}</span>
</div>
</div>
</div>
<div className="dashboard-card">
<div className="card-header"><h3>Permissions</h3></div>
<div className="settings-section">
<div className="permissions-list">
{user?.permissions?.map(p => (
<span key={p} className="permission-badge">{p}</span>
)) || <span>No permissions</span>}
</div>
</div>
</div>
<div className="dashboard-card">
<div className="card-header"><h3>Reporting Preferences</h3></div>
<div className="settings-section">
<div className="setting-row">
<span className="setting-label">Default Standard</span>
<span className="setting-value">IFRS</span>
</div>
<div className="setting-row">
<span className="setting-label">Base Currency</span>
<span className="setting-value">USD</span>
</div>
<div className="setting-row">
<span className="setting-label">Fiscal Year End</span>
<span className="setting-value">December 31</span>
</div>
<div className="setting-row">
<span className="setting-label">Auto-generate Reports</span>
<span className="setting-value">Monthly</span>
</div>
</div>
</div>
<div className="dashboard-card">
<div className="card-header"><h3>Enterprise Controls</h3></div>
<div className="settings-section">
<div className="setting-row">
<span className="setting-label">Multi-signature Required</span>
<span className="setting-value">Yes (2-of-3)</span>
</div>
<div className="setting-row">
<span className="setting-label">Transaction Limit</span>
<span className="setting-value">$10,000,000</span>
</div>
<div className="setting-row">
<span className="setting-label">Approval Workflow</span>
<span className="setting-value">Dual Authorization</span>
</div>
<div className="setting-row">
<span className="setting-label">Session Timeout</span>
<span className="setting-value">30 minutes</span>
</div>
<div className="setting-row">
<span className="setting-label">Audit Logging</span>
<span className="setting-value">Enabled (Full)</span>
</div>
</div>
</div>
</div>
</div>
);
}

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,77 @@
import {
Blocks, Coins, LayoutTemplate, ShieldCheck, Route, Globe,
Bot, Terminal, History, Settings
} from 'lucide-react';
import type { ActivityTab } from '../types';
const tabs: { id: ActivityTab; icon: typeof Blocks; label: string }[] = [
{ id: 'builder', icon: Blocks, label: 'Builder' },
{ id: 'assets', icon: Coins, label: 'Assets' },
{ id: 'templates', icon: LayoutTemplate, label: 'Templates' },
{ id: 'compliance', icon: ShieldCheck, label: 'Compliance' },
{ id: 'routes', icon: Route, label: 'Routes' },
{ id: 'protocols', icon: Globe, label: 'Protocols' },
{ id: 'agents', icon: Bot, label: 'Agents' },
{ id: 'terminal', icon: Terminal, label: 'Terminal' },
{ id: 'audit', icon: History, label: 'Audit' },
{ id: 'settings', icon: Settings, label: 'Settings' },
];
interface ActivityBarProps {
activeTab: ActivityTab;
onTabChange: (tab: ActivityTab) => void;
leftPanelOpen: boolean;
onToggleLeftPanel: () => void;
}
export default function ActivityBar({ activeTab, onTabChange, leftPanelOpen, onToggleLeftPanel }: ActivityBarProps) {
return (
<div className="activity-bar">
<div className="activity-bar-top">
{tabs.slice(0, 7).map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
className={`activity-btn ${isActive && leftPanelOpen ? 'active' : ''}`}
title={tab.label}
onClick={() => {
if (isActive && leftPanelOpen) {
onToggleLeftPanel();
} else {
onTabChange(tab.id);
if (!leftPanelOpen) onToggleLeftPanel();
}
}}
>
<Icon size={20} />
</button>
);
})}
</div>
<div className="activity-bar-bottom">
{tabs.slice(7).map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
className={`activity-btn ${activeTab === tab.id && leftPanelOpen ? 'active' : ''}`}
title={tab.label}
onClick={() => {
if (activeTab === tab.id && leftPanelOpen) {
onToggleLeftPanel();
} else {
onTabChange(tab.id);
if (!leftPanelOpen) onToggleLeftPanel();
}
}}
>
<Icon size={20} />
</button>
);
})}
</div>
</div>
);
}

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

353
src/components/Canvas.tsx Normal file
View 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>
);
}

View File

@@ -0,0 +1,168 @@
import { useState, useEffect, useRef } from 'react';
import { Search, ArrowRight } from 'lucide-react';
interface Command {
id: string;
label: string;
category: string;
shortcut?: string;
}
const commands: Command[] = [
{ id: 'validate', label: 'Run Validation', category: 'Actions', shortcut: 'Ctrl+Shift+V' },
{ id: 'simulate', label: 'Run Simulation', category: 'Actions', shortcut: 'Ctrl+Shift+S' },
{ id: 'execute', label: 'Execute Transaction', category: 'Actions', shortcut: 'Ctrl+Shift+E' },
{ id: 'toggle-left', label: 'Toggle Left Panel', category: 'View', shortcut: 'Ctrl+B' },
{ id: 'toggle-right', label: 'Toggle Right Panel', category: 'View', shortcut: 'Ctrl+J' },
{ id: 'toggle-bottom', label: 'Toggle Bottom Panel', category: 'View', shortcut: 'Ctrl+`' },
{ id: 'search-components', label: 'Search Components', category: 'Navigation' },
{ id: 'new-transaction', label: 'New Transaction', category: 'File', shortcut: 'Ctrl+N' },
{ id: 'save', label: 'Save Transaction', category: 'File', shortcut: 'Ctrl+S' },
{ id: 'export', label: 'Export Transaction', category: 'File' },
{ id: 'import-template', label: 'Import Template', category: 'File' },
{ id: 'focus-chat', label: 'Focus Chat Panel', category: 'Navigation', shortcut: 'Ctrl+/' },
{ id: 'focus-terminal', label: 'Focus Terminal', category: 'Navigation' },
{ id: 'compliance-pass', label: 'Run Compliance Pass', category: 'Compliance' },
{ id: 'optimize-route', label: 'Optimize Routes', category: 'Routing' },
{ id: 'gen-iso', label: 'Generate ISO-20022 Message', category: 'Messaging' },
{ id: 'audit-export', label: 'Export Audit Summary', category: 'Audit' },
];
interface CommandPaletteProps {
isOpen: boolean;
onClose: () => void;
onToggleLeft: () => void;
onToggleRight: () => void;
onToggleBottom: () => void;
onValidate: () => void;
onSimulate: () => void;
onExecute: () => void;
onNewTransaction: () => void;
onFocusChat: () => void;
onFocusTerminal: () => void;
onRunCompliance: () => void;
onOptimizeRoute: () => void;
onGenerateISO: () => void;
onExportAudit: () => void;
onSearchComponents: () => void;
}
export default function CommandPalette({
isOpen, onClose,
onToggleLeft, onToggleRight, onToggleBottom,
onValidate, onSimulate, onExecute,
onNewTransaction, onFocusChat, onFocusTerminal,
onRunCompliance, onOptimizeRoute, onGenerateISO, onExportAudit,
onSearchComponents,
}: CommandPaletteProps) {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setQuery('');
setSelectedIndex(0);
setTimeout(() => inputRef.current?.focus(), 50);
}
}, [isOpen]);
if (!isOpen) return null;
const filtered = commands.filter(c =>
c.label.toLowerCase().includes(query.toLowerCase()) ||
c.category.toLowerCase().includes(query.toLowerCase())
);
const grouped = filtered.reduce<Record<string, Command[]>>((acc, cmd) => {
if (!acc[cmd.category]) acc[cmd.category] = [];
acc[cmd.category].push(cmd);
return acc;
}, {});
const flatList = Object.values(grouped).flat();
const executeCommand = (id: string) => {
switch (id) {
case 'toggle-left': onToggleLeft(); break;
case 'toggle-right': onToggleRight(); break;
case 'toggle-bottom': onToggleBottom(); break;
case 'validate': onValidate(); break;
case 'simulate': onSimulate(); break;
case 'execute': onExecute(); break;
case 'new-transaction': onNewTransaction(); break;
case 'focus-chat': onFocusChat(); break;
case 'focus-terminal': onFocusTerminal(); break;
case 'compliance-pass': onRunCompliance(); break;
case 'optimize-route': onOptimizeRoute(); break;
case 'gen-iso': onGenerateISO(); break;
case 'audit-export': onExportAudit(); break;
case 'search-components': onSearchComponents(); break;
case 'save': /* already auto-saved */ break;
case 'export': /* export handled */ break;
case 'import-template': /* import handled */ break;
}
onClose();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') { onClose(); return; }
if (e.key === 'Enter' && flatList.length > 0) {
executeCommand(flatList[selectedIndex]?.id || flatList[0].id);
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(prev => Math.min(prev + 1, flatList.length - 1));
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(prev => Math.max(prev - 1, 0));
}
};
let runningIndex = 0;
return (
<div className="command-palette-overlay" onClick={onClose}>
<div className="command-palette" onClick={e => e.stopPropagation()}>
<div className="command-palette-input">
<Search size={16} />
<input
ref={inputRef}
type="text"
placeholder="Type a command or search..."
value={query}
onChange={e => { setQuery(e.target.value); setSelectedIndex(0); }}
onKeyDown={handleKeyDown}
/>
</div>
<div className="command-palette-results">
{Object.entries(grouped).map(([category, cmds]) => (
<div key={category} className="command-group">
<div className="command-group-header">{category}</div>
{cmds.map(cmd => {
const idx = runningIndex++;
return (
<div
key={cmd.id}
className={`command-item ${idx === selectedIndex ? 'selected' : ''}`}
onClick={() => executeCommand(cmd.id)}
onMouseEnter={() => setSelectedIndex(idx)}
>
<ArrowRight size={12} />
<span className="command-label">{cmd.label}</span>
{cmd.shortcut && <kbd className="command-shortcut">{cmd.shortcut}</kbd>}
</div>
);
})}
</div>
))}
{filtered.length === 0 && (
<div className="command-empty">No commands found</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,334 @@
import { useState, type DragEvent } from 'react';
import { Search, Star, Clock, ChevronRight, ChevronDown, GripVertical } from 'lucide-react';
import { componentCategories, componentItems } from '../data/components';
import type { ComponentItem, ActivityTab } from '../types';
interface LeftPanelProps {
width: number;
activityTab: ActivityTab;
recentComponents: string[];
}
const activityTabLabels: Record<ActivityTab, string> = {
builder: 'Components',
assets: 'Assets',
templates: 'Templates',
compliance: 'Compliance',
routes: 'Routes',
protocols: 'Protocols',
agents: 'Agents',
terminal: 'Terminal',
audit: 'Audit',
settings: 'Settings',
};
const activityTabCategories: Partial<Record<ActivityTab, string[]>> = {
assets: ['assets'],
templates: ['templates'],
compliance: ['compliance'],
routes: ['routing'],
protocols: ['messaging'],
};
export default function LeftPanel({ width, activityTab, recentComponents }: LeftPanelProps) {
const [search, setSearch] = useState('');
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set(componentCategories.map(c => c.id))
);
const [favorites, setFavorites] = useState<Set<string>>(new Set(['transfer', 'swap', 'kyc']));
const [activeFilter, setActiveFilter] = useState<'all' | 'favorites' | 'recent'>('all');
const [tooltipItem, setTooltipItem] = useState<ComponentItem | null>(null);
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
const toggleCategory = (id: string) => {
const next = new Set(expandedCategories);
if (next.has(id)) next.delete(id); else next.add(id);
setExpandedCategories(next);
};
const toggleFavorite = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
const next = new Set(favorites);
if (next.has(id)) next.delete(id); else next.add(id);
setFavorites(next);
};
const onDragStart = (e: DragEvent, item: ComponentItem) => {
e.dataTransfer.setData('application/transactflow-component', JSON.stringify(item));
e.dataTransfer.effectAllowed = 'move';
// Create drag preview
const preview = document.createElement('div');
preview.className = 'drag-preview';
preview.innerHTML = `<span>${item.icon}</span> <span>${item.label}</span>`;
preview.style.cssText = 'position:fixed;top:-100px;left:-100px;background:#1a1a20;border:1px solid #3b82f6;border-radius:6px;padding:6px 12px;color:#e4e4e8;font-size:12px;display:flex;align-items:center;gap:6px;z-index:10000;pointer-events:none;';
document.body.appendChild(preview);
e.dataTransfer.setDragImage(preview, 0, 0);
setTimeout(() => document.body.removeChild(preview), 0);
};
const showTooltip = (item: ComponentItem, e: React.MouseEvent) => {
setTooltipItem(item);
setTooltipPos({ x: e.clientX + 12, y: e.clientY - 10 });
};
const hideTooltip = () => setTooltipItem(null);
// For non-builder tabs, show filtered content
if (activityTab !== 'builder') {
const categoryFilter = activityTabCategories[activityTab];
if (activityTab === 'settings') {
return (
<div className="left-panel" style={{ width }}>
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
<div className="left-panel-content">
<div className="settings-panel">
<div className="settings-group">
<div className="settings-group-header">Workspace</div>
<div className="settings-item"><span>Theme</span><span className="settings-value">Dark</span></div>
<div className="settings-item"><span>Font Size</span><span className="settings-value">13px</span></div>
<div className="settings-item"><span>Snap to Grid</span><span className="settings-value">Enabled</span></div>
<div className="settings-item"><span>Grid Size</span><span className="settings-value">16px</span></div>
</div>
<div className="settings-group">
<div className="settings-group-header">Canvas</div>
<div className="settings-item"><span>Auto-save</span><span className="settings-value">On</span></div>
<div className="settings-item"><span>Minimap</span><span className="settings-value">Visible</span></div>
<div className="settings-item"><span>Animations</span><span className="settings-value">Enabled</span></div>
</div>
<div className="settings-group">
<div className="settings-group-header">Compliance</div>
<div className="settings-item"><span>Auto-validate</span><span className="settings-value">Off</span></div>
<div className="settings-item"><span>Jurisdiction</span><span className="settings-value">Multi</span></div>
</div>
</div>
</div>
</div>
);
}
if (activityTab === 'terminal' || activityTab === 'audit') {
return (
<div className="left-panel" style={{ width }}>
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
<div className="left-panel-content">
<div className="empty-state">
<p>{activityTab === 'terminal' ? 'Terminal output is shown in the bottom panel.' : 'Audit trail is shown in the bottom panel.'}</p>
<p style={{ marginTop: 8, fontSize: 11 }}>Use <kbd style={{ background: '#1a1a20', border: '1px solid #2a2a32', borderRadius: 3, padding: '1px 4px', fontSize: 10 }}>Ctrl+`</kbd> to toggle the bottom panel.</p>
</div>
</div>
</div>
);
}
if (activityTab === 'agents') {
const agentList = [
{ name: 'Builder Agent', desc: 'Helps construct transaction flows', color: '#3b82f6' },
{ name: 'Compliance Agent', desc: 'Monitors policy violations', color: '#22c55e' },
{ name: 'Routing Agent', desc: 'Optimizes execution paths', color: '#f97316' },
{ name: 'ISO-20022 Agent', desc: 'Generates messaging payloads', color: '#a855f7' },
{ name: 'Settlement Agent', desc: 'Manages settlement instructions', color: '#eab308' },
{ name: 'Risk Agent', desc: 'Evaluates transaction risk', color: '#ef4444' },
{ name: 'Documentation Agent', desc: 'Generates deal memos', color: '#6b7280' },
];
return (
<div className="left-panel" style={{ width }}>
<div className="panel-header"><span className="panel-title">Agents</span></div>
<div className="left-panel-content">
{agentList.map(a => (
<div key={a.name} className="agent-list-item">
<div className="agent-list-dot" style={{ background: a.color }} />
<div>
<div className="agent-list-name">{a.name}</div>
<div className="agent-list-desc">{a.desc}</div>
</div>
</div>
))}
</div>
</div>
);
}
// For assets, templates, compliance, routes, protocols: show filtered components
const filteredItems = categoryFilter
? componentItems.filter(i => categoryFilter.includes(i.category))
: componentItems;
const searchFiltered = filteredItems.filter(item =>
item.label.toLowerCase().includes(search.toLowerCase()) ||
item.description.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="left-panel" style={{ width }}>
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
<div className="left-panel-search">
<Search size={14} className="search-icon" />
<input
type="text"
placeholder={`Search ${activityTabLabels[activityTab].toLowerCase()}...`}
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className="left-panel-content">
<div className="category-items flat">
{searchFiltered.map(item => (
<div
key={item.id}
className="component-item"
draggable
onDragStart={e => onDragStart(e, item)}
onMouseEnter={e => showTooltip(item, e)}
onMouseLeave={hideTooltip}
>
<GripVertical size={12} className="drag-handle" />
<span className="component-icon">{item.icon}</span>
<span className="component-label">{item.label}</span>
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
<Star size={11} />
</button>
</div>
))}
{searchFiltered.length === 0 && <div className="empty-state">No items found</div>}
</div>
</div>
{tooltipItem && (
<div className="component-tooltip" style={{ top: tooltipPos.y, left: tooltipPos.x }}>
<div className="tooltip-header">{tooltipItem.icon} {tooltipItem.label}</div>
<div className="tooltip-desc">{tooltipItem.description}</div>
<div className="tooltip-meta">
<span className="tooltip-cat" style={{ color: tooltipItem.color }}>
{componentCategories.find(c => c.id === tooltipItem.category)?.label}
</span>
</div>
{tooltipItem.inputs && <div className="tooltip-fields"><strong>Inputs:</strong> {tooltipItem.inputs.join(', ')}</div>}
{tooltipItem.outputs && <div className="tooltip-fields"><strong>Outputs:</strong> {tooltipItem.outputs.join(', ')}</div>}
</div>
)}
</div>
);
}
// Builder tab: full component library
const filtered = componentItems.filter(item =>
item.label.toLowerCase().includes(search.toLowerCase()) ||
item.description.toLowerCase().includes(search.toLowerCase())
);
const displayItems = activeFilter === 'favorites'
? filtered.filter(i => favorites.has(i.id))
: activeFilter === 'recent'
? filtered.filter(i => recentComponents.includes(i.id)).sort((a, b) => recentComponents.indexOf(a.id) - recentComponents.indexOf(b.id))
: filtered;
return (
<div className="left-panel" style={{ width }}>
<div className="panel-header">
<span className="panel-title">Components</span>
</div>
<div className="left-panel-search">
<Search size={14} className="search-icon" />
<input
type="text"
placeholder="Search components..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className="left-panel-filters">
<button className={`filter-btn ${activeFilter === 'all' ? 'active' : ''}`} onClick={() => setActiveFilter('all')}>All</button>
<button className={`filter-btn ${activeFilter === 'favorites' ? 'active' : ''}`} onClick={() => setActiveFilter('favorites')}>
<Star size={11} /> Favorites
</button>
<button className={`filter-btn ${activeFilter === 'recent' ? 'active' : ''}`} onClick={() => setActiveFilter('recent')}>
<Clock size={11} /> Recent
</button>
</div>
<div className="left-panel-content">
{activeFilter === 'all' && !search ? (
componentCategories.map(cat => {
const catItems = displayItems.filter(i => i.category === cat.id);
if (catItems.length === 0) return null;
const isExpanded = expandedCategories.has(cat.id);
return (
<div key={cat.id} className="component-category">
<div className="category-header" onClick={() => toggleCategory(cat.id)}>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<span className="category-icon">{cat.icon}</span>
<span className="category-label">{cat.label}</span>
<span className="category-count">{catItems.length}</span>
</div>
{isExpanded && (
<div className="category-items">
{catItems.map(item => (
<div
key={item.id}
className="component-item"
draggable
onDragStart={e => onDragStart(e, item)}
onMouseEnter={e => showTooltip(item, e)}
onMouseLeave={hideTooltip}
>
<GripVertical size={12} className="drag-handle" />
<span className="component-icon">{item.icon}</span>
<span className="component-label">{item.label}</span>
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
<Star size={11} />
</button>
</div>
))}
</div>
)}
</div>
);
})
) : (
<div className="category-items flat">
{displayItems.map(item => (
<div
key={item.id}
className="component-item"
draggable
onDragStart={e => onDragStart(e, item)}
onMouseEnter={e => showTooltip(item, e)}
onMouseLeave={hideTooltip}
>
<GripVertical size={12} className="drag-handle" />
<span className="component-icon">{item.icon}</span>
<span className="component-label">{item.label}</span>
<span className="component-category-badge">
{componentCategories.find(c => c.id === item.category)?.label}
</span>
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
<Star size={11} />
</button>
</div>
))}
{displayItems.length === 0 && (
<div className="empty-state">
{activeFilter === 'recent' ? 'No recently used components. Drag a component to the canvas to see it here.' : 'No matching components found.'}
</div>
)}
</div>
)}
</div>
{tooltipItem && (
<div className="component-tooltip" style={{ top: tooltipPos.y, left: tooltipPos.x }}>
<div className="tooltip-header">{tooltipItem.icon} {tooltipItem.label}</div>
<div className="tooltip-desc">{tooltipItem.description}</div>
<div className="tooltip-meta">
<span className="tooltip-cat" style={{ color: tooltipItem.color }}>
{componentCategories.find(c => c.id === tooltipItem.category)?.label}
</span>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,370 @@
import { useState, useRef, useEffect } from 'react';
import {
Send, Sparkles, Wrench, ShieldCheck, Route, FileText,
Landmark, AlertTriangle, BookOpen, ChevronDown, Plus,
Zap, RefreshCw, FileOutput, MessageSquare,
History, Target
} from 'lucide-react';
import type { Agent, ChatMessage, ConversationScope, ComponentItem } from '../types';
import { sampleMessages, sampleThreads } from '../data/sampleData';
import { componentItems } from '../data/components';
import type { Node, Edge } from '@xyflow/react';
const agents: { id: Agent; icon: typeof Sparkles; color: string }[] = [
{ id: 'Builder', icon: Sparkles, color: '#3b82f6' },
{ id: 'Compliance', icon: ShieldCheck, color: '#22c55e' },
{ id: 'Routing', icon: Route, color: '#f97316' },
{ id: 'ISO-20022', icon: FileText, color: '#a855f7' },
{ id: 'Settlement', icon: Landmark, color: '#eab308' },
{ id: 'Risk', icon: AlertTriangle, color: '#ef4444' },
{ id: 'Documentation', icon: BookOpen, color: '#6b7280' },
];
interface RightPanelProps {
width: number;
nodes: Node[];
edges: Edge[];
selectedNodes: Node[];
chatInputRef: React.RefObject<HTMLInputElement | null>;
onInsertBlock: (item: ComponentItem, position: { x: number; y: number }) => void;
onRunValidation: () => void;
onOptimizeRoute: () => void;
onRunCompliance: () => void;
onGenerateSettlement: () => void;
}
export default function RightPanel({
width, nodes, edges, selectedNodes, chatInputRef,
onInsertBlock, onRunValidation, onOptimizeRoute, onRunCompliance, onGenerateSettlement,
}: RightPanelProps) {
const [activeAgent, setActiveAgent] = useState<Agent>('Builder');
const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);
const [input, setInput] = useState('');
const [showAgentMenu, setShowAgentMenu] = useState(false);
const [showContext, setShowContext] = useState(false);
const [showThreads, setShowThreads] = useState(false);
const [scope, setScope] = useState<ConversationScope>('full-transaction');
const [showScopeMenu, setShowScopeMenu] = useState(false);
const messagesEnd = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const getCanvasContext = () => {
const nodeLabels = nodes.map(n => (n.data as Record<string, unknown>).label as string);
const categories = [...new Set(nodes.map(n => (n.data as Record<string, unknown>).category as string))];
const hasCompliance = categories.includes('compliance');
const hasRouting = categories.includes('routing');
const disconnected = nodes.filter(n => !edges.some(e => e.source === n.id || e.target === n.id));
const selectedLabels = selectedNodes.map(n => (n.data as Record<string, unknown>).label as string);
return { nodeLabels, categories, hasCompliance, hasRouting, disconnected, selectedLabels, nodeCount: nodes.length, edgeCount: edges.length };
};
const sendMessage = () => {
if (!input.trim()) return;
const userMsg: ChatMessage = {
id: Date.now().toString(),
agent: 'User',
content: input,
timestamp: new Date(),
type: 'user',
};
setMessages(prev => [...prev, userMsg]);
const capturedInput = input;
setInput('');
setTimeout(() => {
const ctx = getCanvasContext();
const buildResponse = (): string => {
const lowerInput = capturedInput.toLowerCase();
switch (activeAgent) {
case 'Builder': {
if (selectedNodes.length > 0) {
const sel = (selectedNodes[0].data as Record<string, unknown>).label as string;
if (lowerInput.includes('explain')) return `The "${sel}" block ${getBlockExplanation(sel)}. It currently has ${edges.filter(e => e.source === selectedNodes[0].id || e.target === selectedNodes[0].id).length} connection(s).`;
if (lowerInput.includes('next') || lowerInput.includes('suggest')) return `After "${sel}", I recommend adding a ${suggestNextBlock(sel)}. This would complete the ${(selectedNodes[0].data as Record<string, unknown>).category} flow.`;
}
if (lowerInput.includes('build') || lowerInput.includes('create') || lowerInput.includes('set up') || lowerInput.includes('payment')) {
if (ctx.nodeCount === 0) return `To build a transaction flow, start by dragging a "Fiat Account" or "Stablecoin Wallet" from the left panel as your source. Then add a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" action and connect them.`;
return `Your graph has ${ctx.nodeCount} nodes. Try dragging a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" block onto the canvas and connecting it to your source.`;
}
if (ctx.disconnected.length > 0) return `I notice ${ctx.disconnected.length} disconnected node(s) in your graph: ${ctx.disconnected.map(n => (n.data as Record<string, unknown>).label).join(', ')}. Connect them to complete the flow.`;
return `I can help you build that flow. Try dragging a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" block onto the canvas and connecting it to your source. Your graph currently has ${ctx.nodeCount} nodes and ${ctx.edgeCount} connections.`;
}
case 'Compliance': {
if (lowerInput.includes('check') || lowerInput.includes('compliance') || lowerInput.includes('review')) {
if (!ctx.hasCompliance && ctx.nodeCount > 0) return `WARNING: Your transaction graph has ${ctx.nodeCount} nodes but no compliance checks. I recommend adding KYC and AML nodes before the settlement step. This is required for cross-border transactions.`;
if (ctx.hasCompliance) return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction. ${ctx.nodeCount} nodes verified against 47 compliance rules.`;
return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction.`;
}
if (lowerInput.includes('violation') || lowerInput.includes('failure')) return `Scanning graph for policy violations... ${ctx.hasCompliance ? 'All compliance nodes are properly configured. No violations found.' : 'No compliance nodes found in graph. Consider adding KYC/AML checks.'}`;
return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction.`;
}
case 'Routing': {
if (ctx.hasRouting) return `Analyzing ${ctx.nodeCount} nodes with routing configuration. Found optimal path via ${ctx.nodeLabels.find(l => l.includes('Route') || l.includes('Router')) || 'Banking Rail'}. Estimated fee: 0.02%, latency: 230ms.`;
return `Analyzing optimal routes... Found 3 execution paths. The best route via Banking Rail offers lowest fees at 0.02%. Your graph has ${ctx.nodeCount} nodes across ${ctx.edgeCount} connections.`;
}
case 'ISO-20022': {
if (ctx.nodeCount > 0) return `Based on your graph with ${ctx.nodeCount} nodes, I can generate a pain.001 message. The required fields from your current configuration: debtor (${ctx.nodeLabels[0] || 'source'}), creditor (${ctx.nodeLabels[ctx.nodeLabels.length - 1] || 'destination'}), amount, and currency.`;
return `I can generate a pain.001 message for this transfer. The required fields based on your current graph are: debtor, creditor, amount, and currency.`;
}
case 'Settlement': {
return `Current settlement window for this transaction type is T+1. ${ctx.nodeCount > 0 ? `Your graph has ${ctx.nodeCount} nodes ready for settlement processing.` : 'I recommend adding a settlement instruction block to specify your preferred CSD.'}`;
}
case 'Risk': {
if (ctx.nodeCount > 0) {
const riskLevel = ctx.hasCompliance ? 'LOW' : 'MEDIUM';
return `Risk assessment: ${riskLevel}. ${ctx.nodeCount} nodes evaluated. ${ctx.hasCompliance ? 'Compliance checks present.' : 'No compliance nodes — risk elevated.'} ${ctx.disconnected.length > 0 ? `${ctx.disconnected.length} disconnected node(s) detected.` : 'All nodes connected.'}`;
}
return `Risk assessment: LOW. Transaction amount is within normal parameters. No counterparty risk flags detected.`;
}
case 'Documentation': {
if (ctx.nodeCount > 0) return `Generating deal memo for "${ctx.nodeLabels[0]}" flow with ${ctx.nodeCount} nodes. Categories: ${ctx.categories.join(', ')}. ${ctx.edgeCount} connections mapped. ${ctx.hasCompliance ? 'Compliance: verified.' : 'Compliance: not yet added.'}`;
return `I'll generate a deal memo for this transaction. It will include the execution path, compliance checks, and settlement instructions.`;
}
default:
return 'How can I assist you?';
}
};
const reply: ChatMessage = {
id: (Date.now() + 1).toString(),
agent: activeAgent,
content: buildResponse(),
timestamp: new Date(),
type: 'agent',
};
setMessages(prev => [...prev, reply]);
}, 800);
};
const currentAgentDef = agents.find(a => a.id === activeAgent)!;
const CurrentIcon = currentAgentDef.icon;
const scopeLabels: Record<ConversationScope, string> = {
'current-node': 'Current Node',
'current-flow': 'Current Flow',
'full-transaction': 'Full Transaction',
'terminal': 'Terminal',
'compliance': 'Compliance Only',
};
// Context from canvas
const ctx = getCanvasContext();
const handleInsertBlock = () => {
const suggestions = ['transfer', 'kyc', 'banking-rail'];
const item = componentItems.find(c => c.id === suggestions[Math.floor(Math.random() * suggestions.length)]);
if (item) onInsertBlock(item, { x: 250 + Math.random() * 200, y: 150 + Math.random() * 200 });
};
return (
<div className="right-panel" style={{ width }}>
<div className="panel-header">
<div className="chat-header-agent" onClick={() => setShowAgentMenu(!showAgentMenu)}>
<CurrentIcon size={14} color={currentAgentDef.color} />
<span>{activeAgent} Agent</span>
<ChevronDown size={12} />
{showAgentMenu && (
<div className="agent-dropdown">
{agents.map(a => {
const Icon = a.icon;
return (
<div
key={a.id}
className={`agent-option ${a.id === activeAgent ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setActiveAgent(a.id); setShowAgentMenu(false); }}
>
<Icon size={14} color={a.color} />
<span>{a.id}</span>
</div>
);
})}
</div>
)}
</div>
<div className="chat-header-actions">
<div className="scope-selector" onClick={() => setShowScopeMenu(!showScopeMenu)}>
<Target size={11} />
<span>{scopeLabels[scope]}</span>
<ChevronDown size={10} />
{showScopeMenu && (
<div className="scope-dropdown">
{(Object.keys(scopeLabels) as ConversationScope[]).map(s => (
<div
key={s}
className={`scope-option ${s === scope ? 'active' : ''}`}
onClick={e => { e.stopPropagation(); setScope(s); setShowScopeMenu(false); }}
>
{scopeLabels[s]}
</div>
))}
</div>
)}
</div>
<button
className={`icon-btn-xs ${showThreads ? 'active' : ''}`}
onClick={() => setShowThreads(!showThreads)}
title="Thread History"
>
<History size={12} />
</button>
<button
className={`context-toggle ${showContext ? 'active' : ''}`}
onClick={() => setShowContext(!showContext)}
title="Toggle context panel"
>
Context
</button>
</div>
</div>
<div className="agent-tabs">
{agents.map(a => {
const Icon = a.icon;
return (
<button
key={a.id}
className={`agent-tab ${a.id === activeAgent ? 'active' : ''}`}
onClick={() => setActiveAgent(a.id)}
title={a.id}
style={a.id === activeAgent ? { borderBottomColor: a.color } : {}}
>
<Icon size={13} color={a.id === activeAgent ? a.color : '#666'} />
</button>
);
})}
</div>
{showThreads && (
<div className="thread-history">
<div className="thread-history-header">Thread History</div>
{sampleThreads.map(t => (
<div key={t.id} className="thread-item" onClick={() => setShowThreads(false)}>
<MessageSquare size={12} color={agents.find(a => a.id === t.agent)?.color} />
<div className="thread-item-content">
<span className="thread-item-title">{t.title}</span>
<span className="thread-item-meta">{t.agent} · {t.messageCount} messages</span>
</div>
</div>
))}
</div>
)}
{showContext && (
<div className="context-panel">
<div className="context-section">
<span className="context-label">Selected</span>
<span className="context-value">{ctx.selectedLabels.length > 0 ? ctx.selectedLabels.join(', ') : 'None'}</span>
</div>
<div className="context-section">
<span className="context-label">Nodes</span>
<span className="context-value">{ctx.nodeCount}</span>
</div>
<div className="context-section">
<span className="context-label">Connections</span>
<span className="context-value">{ctx.edgeCount}</span>
</div>
<div className="context-section">
<span className="context-label">Jurisdiction</span>
<span className="context-value">Multi</span>
</div>
<div className="context-section">
<span className="context-label">Counterparties</span>
<span className="context-value">{ctx.nodeCount > 0 ? Math.max(1, Math.floor(ctx.nodeCount / 3)) : 0}</span>
</div>
<div className="context-section">
<span className="context-label">Compliance</span>
<span className={`context-value ${ctx.hasCompliance ? 'pass' : (ctx.nodeCount > 0 ? 'warn' : '')}`}>
{ctx.hasCompliance ? 'Pass' : (ctx.nodeCount > 0 ? 'Missing' : 'N/A')}
</span>
</div>
<div className="context-section">
<span className="context-label">Categories</span>
<span className="context-value">{ctx.categories.length > 0 ? ctx.categories.join(', ') : '—'}</span>
</div>
<div className="context-section">
<span className="context-label">Est. Fees</span>
<span className="context-value">{ctx.nodeCount > 0 ? '$0.02%' : '—'}</span>
</div>
</div>
)}
<div className="chat-messages">
{messages.map(msg => (
<div key={msg.id} className={`chat-message ${msg.type}`}>
<div className="message-header">
<span className="message-agent">{msg.agent}</span>
<span className="message-time">
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="message-content">{msg.content}</div>
</div>
))}
<div ref={messagesEnd} />
</div>
<div className="action-tray">
<button className="action-tray-btn" title="Insert recommended block" onClick={handleInsertBlock}>
<Plus size={12} /> Insert Block
</button>
<button className="action-tray-btn" title="Repair graph" onClick={onRunValidation}>
<Wrench size={12} /> Repair
</button>
<button className="action-tray-btn" title="Optimize route" onClick={onOptimizeRoute}>
<Zap size={12} /> Optimize
</button>
<button className="action-tray-btn" title="Run compliance" onClick={onRunCompliance}>
<ShieldCheck size={12} /> Comply
</button>
<button className="action-tray-btn" title="Generate settlement message" onClick={onGenerateSettlement}>
<FileOutput size={12} /> Settle
</button>
<button className="action-tray-btn" title="Refresh context" onClick={() => setShowContext(true)}>
<RefreshCw size={12} />
</button>
</div>
<div className="chat-input-area">
<input
ref={chatInputRef}
type="text"
placeholder={`Ask ${activeAgent} Agent...`}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && sendMessage()}
/>
<button className="send-btn" onClick={sendMessage} disabled={!input.trim()}>
<Send size={14} />
</button>
</div>
</div>
);
}
function getBlockExplanation(label: string): string {
const explanations: Record<string, string> = {
'Fiat Account': 'represents a traditional fiat currency account, typically used as a source or destination for fund transfers',
'Transfer': 'moves value from one account to another along a defined path',
'KYC': 'performs Know Your Customer verification before allowing the transaction to proceed',
'AML': 'runs Anti-Money Laundering screening against watchlists',
'Swap': 'exchanges one asset type for another at the current market rate',
'Banking Rail': 'routes the transaction through traditional banking infrastructure',
};
return explanations[label] || 'is a transaction primitive used in flow composition';
}
function suggestNextBlock(label: string): string {
const suggestions: Record<string, string> = {
'Fiat Account': 'Transfer or Convert block',
'Transfer': 'KYC compliance check',
'KYC': 'AML screening node',
'AML': 'Banking Rail or DEX Route',
'Swap': 'Settlement instruction',
'Banking Rail': 'Settlement instruction',
};
return suggestions[label] || 'compliance or routing node';
}

166
src/components/TitleBar.tsx Normal file
View File

@@ -0,0 +1,166 @@
import { useState } from 'react';
import {
Search, Bell, ChevronDown, Play, FlaskConical, ShieldCheck, Zap,
Command, User, LogOut, Settings, Shield
} from 'lucide-react';
import type { SessionMode } from '../types';
import { sampleNotifications } from '../data/sampleData';
const modeColors: Record<SessionMode, string> = {
Sandbox: '#eab308',
Simulate: '#3b82f6',
Live: '#22c55e',
'Compliance Review': '#a855f7',
};
interface TitleBarProps {
mode: SessionMode;
onModeChange: (mode: SessionMode) => void;
onToggleCommandPalette: () => void;
onValidate: () => void;
onSimulate: () => void;
onExecute: () => void;
}
export default function TitleBar({ mode, onModeChange, onToggleCommandPalette, onValidate, onSimulate, onExecute }: TitleBarProps) {
const [showModeMenu, setShowModeMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const [notifications, setNotifications] = useState(sampleNotifications);
const modes: SessionMode[] = ['Sandbox', 'Simulate', 'Live', 'Compliance Review'];
const unreadCount = notifications.filter(n => !n.read).length;
const markAllRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
};
const notifTypeColors: Record<string, string> = {
info: '#3b82f6',
success: '#22c55e',
warning: '#eab308',
error: '#ef4444',
};
return (
<div className="title-bar">
<div className="title-bar-left">
<div className="title-bar-logo">
<Zap size={18} color="#3b82f6" />
<span className="title-bar-name">TransactFlow</span>
</div>
<div className="title-bar-separator" />
<span className="title-bar-workspace">Institutional Workspace</span>
<div className="title-bar-separator" />
<div className="mode-selector" onClick={() => setShowModeMenu(!showModeMenu)}>
<div className="mode-dot" style={{ background: modeColors[mode] }} />
<span>{mode}</span>
<ChevronDown size={12} />
{showModeMenu && (
<div className="mode-dropdown">
{modes.map(m => (
<div
key={m}
className={`mode-option ${m === mode ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); onModeChange(m); setShowModeMenu(false); }}
>
<div className="mode-dot" style={{ background: modeColors[m] }} />
{m}
</div>
))}
</div>
)}
</div>
</div>
<div className="title-bar-center">
<button className="title-bar-search" onClick={onToggleCommandPalette}>
<Search size={13} />
<span>Search or run command...</span>
<kbd>Ctrl+K</kbd>
</button>
</div>
<div className="title-bar-right">
<button className="title-bar-action validate" title="Validate (Ctrl+Shift+V)" onClick={onValidate}>
<ShieldCheck size={15} />
<span>Validate</span>
</button>
<button className="title-bar-action simulate" title="Simulate (Ctrl+Shift+S)" onClick={onSimulate}>
<FlaskConical size={15} />
<span>Simulate</span>
</button>
<button className="title-bar-action execute" title="Execute (Ctrl+Shift+E)" onClick={onExecute}>
<Play size={15} />
<span>Execute</span>
</button>
<div className="title-bar-separator" />
{/* Notification bell with dropdown */}
<div className="notification-wrapper">
<button className="icon-btn" title="Notifications" onClick={() => { setShowNotifications(!showNotifications); setShowUserMenu(false); }}>
<Bell size={16} />
{unreadCount > 0 && <span className="notification-badge">{unreadCount}</span>}
</button>
{showNotifications && (
<div className="notification-dropdown">
<div className="notification-dropdown-header">
<span>Notifications</span>
{unreadCount > 0 && (
<button className="mark-read-btn" onClick={markAllRead}>Mark all read</button>
)}
</div>
{notifications.map(n => (
<div key={n.id} className={`notification-item ${n.read ? 'read' : ''}`} onClick={() => setNotifications(prev => prev.map(x => x.id === n.id ? { ...x, read: true } : x))}>
<div className="notification-dot" style={{ background: notifTypeColors[n.type] }} />
<div className="notification-content">
<span className="notification-title">{n.title}</span>
<span className="notification-message">{n.message}</span>
<span className="notification-time">{n.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
</div>
</div>
))}
</div>
)}
</div>
<button className="icon-btn" title="Command Palette" onClick={onToggleCommandPalette}>
<Command size={16} />
</button>
{/* User menu with dropdown */}
<div className="user-menu-wrapper">
<button className="icon-btn user-btn" title="User Settings" onClick={() => { setShowUserMenu(!showUserMenu); setShowNotifications(false); }}>
<User size={16} />
</button>
{showUserMenu && (
<div className="user-menu-dropdown">
<div className="user-menu-profile">
<div className="user-avatar">JD</div>
<div>
<div className="user-name">Jane Doe</div>
<div className="user-role">Admin · Compliance Officer</div>
</div>
</div>
<div className="user-menu-divider" />
<div className="user-menu-item">
<User size={13} /> <span>Profile</span>
</div>
<div className="user-menu-item">
<Settings size={13} /> <span>Settings</span>
</div>
<div className="user-menu-item">
<Shield size={13} /> <span>Permissions</span>
<span className="user-menu-badge">Admin</span>
</div>
<div className="user-menu-divider" />
<div className="user-menu-item logout">
<LogOut size={13} /> <span>Sign Out</span>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { memo } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
import { AlertTriangle, CheckCircle2, XCircle, Shield } from 'lucide-react';
type TransactionNodeData = {
label: string;
category: string;
icon: string;
color: string;
status?: 'valid' | 'warning' | 'error';
};
const complianceCategories = ['compliance'];
const routingCategories = ['routing'];
function TransactionNodeComponent({ data, selected }: NodeProps) {
const nodeData = data as unknown as TransactionNodeData;
const statusIcon = nodeData.status === 'valid' ? <CheckCircle2 size={10} color="#22c55e" /> :
nodeData.status === 'warning' ? <AlertTriangle size={10} color="#eab308" /> :
nodeData.status === 'error' ? <XCircle size={10} color="#ef4444" /> : null;
const isCompliance = complianceCategories.includes(nodeData.category);
const isRouting = routingCategories.includes(nodeData.category);
return (
<div className={`transaction-node ${selected ? 'selected' : ''} ${nodeData.status ? `status-${nodeData.status}` : ''}`}
style={{ borderColor: selected ? '#3b82f6' : nodeData.color + '60' }}>
<Handle type="target" position={Position.Left} className="node-handle" />
<div className="node-header" style={{ borderBottomColor: nodeData.color + '30' }}>
<span className="node-icon">{nodeData.icon}</span>
<span className="node-label">{nodeData.label}</span>
<div className="node-badges">
{isCompliance && (
<span className="node-badge compliance" title="Compliance node">
<Shield size={8} />
</span>
)}
{isRouting && (
<span className="node-badge routing" title="Routing node">
🔀
</span>
)}
{statusIcon && <span className="node-status">{statusIcon}</span>}
</div>
</div>
<div className="node-body">
<span className="node-category" style={{ color: nodeData.color }}>{nodeData.category}</span>
</div>
<Handle type="source" position={Position.Right} className="node-handle" />
</div>
);
}
export default memo(TransactionNodeComponent);

View File

@@ -0,0 +1,175 @@
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import {
LayoutDashboard, Zap, Building2, Landmark, FileText, Shield, CheckSquare,
Settings, LogOut, ChevronLeft, ChevronRight, Bell, User, Copy,
ExternalLink, ChevronDown
} from 'lucide-react';
const navItems = [
{ id: 'dashboard', label: 'Overview', icon: LayoutDashboard, path: '/dashboard' },
{ id: 'transaction-builder', label: 'Transaction Builder', icon: Zap, path: '/transaction-builder' },
{ id: 'accounts', label: 'Accounts', icon: Building2, path: '/accounts' },
{ id: 'treasury', label: 'Treasury', icon: Landmark, path: '/treasury' },
{ id: 'reporting', label: 'Reporting', icon: FileText, path: '/reporting' },
{ id: 'compliance', label: 'Compliance & Risk', icon: Shield, path: '/compliance' },
{ id: 'settlements', label: 'Settlements', icon: CheckSquare, path: '/settlements' },
];
interface PortalLayoutProps {
children: React.ReactNode;
}
export default function PortalLayout({ children }: PortalLayoutProps) {
const { user, wallet, disconnect } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const currentPath = location.pathname;
const copyAddress = () => {
if (wallet?.address) {
navigator.clipboard.writeText(wallet.address);
}
};
return (
<div className="portal-layout">
<div className="portal-topbar">
<div className="portal-topbar-left">
<div className="portal-logo" onClick={() => navigate('/dashboard')}>
<Building2 size={22} color="#3b82f6" />
{!collapsed && (
<div className="portal-logo-text">
<span className="portal-logo-name">Solace Bank Group</span>
<span className="portal-logo-plc">PLC</span>
</div>
)}
</div>
</div>
<div className="portal-topbar-center">
<div className="portal-env-badge">
<span className="env-dot" />
Production
</div>
</div>
<div className="portal-topbar-right">
<div className="portal-notif-wrapper">
<button className="portal-icon-btn" onClick={() => { setShowNotifications(!showNotifications); setShowUserMenu(false); }}>
<Bell size={18} />
<span className="portal-notif-badge">3</span>
</button>
{showNotifications && (
<div className="portal-dropdown notifications-dropdown">
<div className="portal-dropdown-header">Notifications</div>
<div className="portal-dropdown-item warning">
<span className="dropdown-dot warning" />
<div>
<div className="dropdown-title">AML Alert</div>
<div className="dropdown-desc">Unusual pattern on ACC-001</div>
</div>
</div>
<div className="portal-dropdown-item info">
<span className="dropdown-dot info" />
<div>
<div className="dropdown-title">Settlement Confirmed</div>
<div className="dropdown-desc">TX-2024-0847 settled</div>
</div>
</div>
<div className="portal-dropdown-item">
<span className="dropdown-dot success" />
<div>
<div className="dropdown-title">Report Ready</div>
<div className="dropdown-desc">Q4 IFRS Balance Sheet</div>
</div>
</div>
</div>
)}
</div>
<div className="portal-user-wrapper">
<button className="portal-user-btn" onClick={() => { setShowUserMenu(!showUserMenu); setShowNotifications(false); }}>
<div className="portal-avatar">
<User size={14} />
</div>
<div className="portal-user-info">
<span className="portal-user-name">{user?.displayName || 'User'}</span>
<span className="portal-user-role">{user?.role?.replace('_', ' ') || 'Admin'}</span>
</div>
<ChevronDown size={12} />
</button>
{showUserMenu && (
<div className="portal-dropdown user-dropdown">
<div className="portal-dropdown-header">Account</div>
<div className="portal-dropdown-section">
<div className="portal-wallet-addr">
<span className="mono">{wallet?.address ? `${wallet.address.slice(0, 8)}...${wallet.address.slice(-6)}` : '—'}</span>
<button className="copy-btn" onClick={copyAddress} title="Copy address"><Copy size={12} /></button>
</div>
<div className="portal-wallet-bal">
<span>{wallet?.balance ? `${parseFloat(wallet.balance).toFixed(4)} ETH` : '—'}</span>
<span className="chain-badge">Chain {wallet?.chainId || 1}</span>
</div>
</div>
<div className="portal-dropdown-divider" />
<button className="portal-dropdown-action" onClick={() => navigate('/settings')}>
<Settings size={14} /> Settings
</button>
<button className="portal-dropdown-action" onClick={() => window.open('https://etherscan.io', '_blank')}>
<ExternalLink size={14} /> View on Explorer
</button>
<div className="portal-dropdown-divider" />
<button className="portal-dropdown-action danger" onClick={disconnect}>
<LogOut size={14} /> Disconnect Wallet
</button>
</div>
)}
</div>
</div>
</div>
<div className="portal-body">
<nav className={`portal-sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="portal-nav-items">
{navItems.map(item => {
const Icon = item.icon;
const isActive = currentPath === item.path || (item.path !== '/dashboard' && currentPath.startsWith(item.path));
return (
<button
key={item.id}
className={`portal-nav-item ${isActive ? 'active' : ''}`}
onClick={() => navigate(item.path)}
title={collapsed ? item.label : undefined}
>
<Icon size={18} />
{!collapsed && <span>{item.label}</span>}
{isActive && <div className="nav-active-indicator" />}
</button>
);
})}
</div>
<div className="portal-nav-footer">
<button className="portal-nav-item" onClick={() => navigate('/settings')} title={collapsed ? 'Settings' : undefined}>
<Settings size={18} />
{!collapsed && <span>Settings</span>}
</button>
<button className="portal-collapse-btn" onClick={() => setCollapsed(!collapsed)}>
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</button>
</div>
</nav>
<main className="portal-content">
{children}
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,153 @@
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react';
import { BrowserProvider, formatEther } from 'ethers';
import type { AuthState, WalletInfo, PortalUser, UserRole, Permission } from '../types/portal';
interface AuthContextType extends AuthState {
connectWallet: (provider: 'metamask' | 'walletconnect' | 'coinbase') => Promise<void>;
disconnect: () => void;
error: string | null;
}
const AuthContext = createContext<AuthContextType | null>(null);
const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
admin: [
'accounts.view', 'accounts.manage', 'accounts.create',
'transactions.view', 'transactions.create', 'transactions.approve', 'transactions.execute',
'treasury.view', 'treasury.manage', 'treasury.rebalance',
'compliance.view', 'compliance.manage', 'compliance.override',
'reports.view', 'reports.generate', 'reports.export',
'settlements.view', 'settlements.approve',
'admin.users', 'admin.settings', 'admin.audit',
],
treasurer: [
'accounts.view', 'accounts.manage',
'transactions.view', 'transactions.create', 'transactions.approve',
'treasury.view', 'treasury.manage', 'treasury.rebalance',
'reports.view', 'reports.generate', 'reports.export',
'settlements.view', 'settlements.approve',
],
analyst: [
'accounts.view', 'transactions.view', 'treasury.view',
'reports.view', 'reports.generate', 'settlements.view',
],
compliance_officer: [
'accounts.view', 'transactions.view', 'treasury.view',
'compliance.view', 'compliance.manage',
'reports.view', 'reports.generate', 'reports.export',
'settlements.view',
],
auditor: [
'accounts.view', 'transactions.view', 'treasury.view',
'compliance.view', 'reports.view', 'reports.export',
'settlements.view', 'admin.audit',
],
viewer: ['accounts.view', 'transactions.view', 'treasury.view', 'reports.view', 'settlements.view'],
};
const AUTH_STORAGE_KEY = 'solace-auth';
function generateUser(address: string): PortalUser {
return {
id: `usr-${address.slice(2, 10)}`,
displayName: `${address.slice(0, 6)}...${address.slice(-4)}`,
role: 'admin',
permissions: ROLE_PERMISSIONS['admin'],
institution: 'Solace Bank Group PLC',
department: 'Treasury Operations',
lastLogin: new Date(),
walletAddress: address,
};
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
wallet: null,
user: null,
loading: true,
});
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const saved = localStorage.getItem(AUTH_STORAGE_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
setState({
isAuthenticated: true,
wallet: parsed.wallet,
user: { ...parsed.user, lastLogin: new Date(parsed.user.lastLogin) },
loading: false,
});
return;
} catch { /* ignore */ }
}
setState(prev => ({ ...prev, loading: false }));
}, []);
const connectWallet = useCallback(async (providerType: 'metamask' | 'walletconnect' | 'coinbase') => {
setError(null);
setState(prev => ({ ...prev, loading: true }));
try {
let address: string;
let chainId: number;
let balance: string;
const ethereum = (window as unknown as Record<string, unknown>).ethereum as {
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
isMetaMask?: boolean;
isCoinbaseWallet?: boolean;
chainId?: string;
} | undefined;
if (ethereum && (providerType === 'metamask' || providerType === 'coinbase')) {
const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) as string[];
if (!accounts || accounts.length === 0) throw new Error('No accounts returned');
const provider = new BrowserProvider(ethereum as never);
const signer = await provider.getSigner();
address = await signer.getAddress();
const network = await provider.getNetwork();
chainId = Number(network.chainId);
const bal = await provider.getBalance(address);
balance = formatEther(bal);
} else {
// Demo mode — simulate wallet connection for environments without MetaMask
await new Promise(resolve => setTimeout(resolve, 1200));
address = '0x' + Array.from({ length: 40 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
chainId = 1;
balance = (Math.random() * 100).toFixed(4);
}
const wallet: WalletInfo = { address, chainId, balance, provider: providerType };
const user = generateUser(address);
const newState: AuthState = { isAuthenticated: true, wallet, user, loading: false };
setState(newState);
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ wallet, user }));
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to connect wallet';
setError(msg);
setState(prev => ({ ...prev, loading: false }));
}
}, []);
const disconnect = useCallback(() => {
setState({ isAuthenticated: false, wallet: null, user: null, loading: false });
localStorage.removeItem(AUTH_STORAGE_KEY);
}, []);
return (
<AuthContext.Provider value={{ ...state, connectWallet, disconnect, error }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}

81
src/data/components.ts Normal file
View File

@@ -0,0 +1,81 @@
import type { ComponentItem } from '../types';
export const componentCategories = [
{ id: 'assets', label: 'Asset Primitives', icon: '💰' },
{ id: 'actions', label: 'Transaction Actions', icon: '⚡' },
{ id: 'routing', label: 'Routing Components', icon: '🔀' },
{ id: 'compliance', label: 'Compliance Components', icon: '🛡️' },
{ id: 'messaging', label: 'ISO-20022 / Messaging', icon: '📨' },
{ id: 'logic', label: 'Logic / Control', icon: '🔧' },
{ id: 'templates', label: 'Templates', icon: '📋' },
];
export const componentItems: ComponentItem[] = [
// Asset Primitives
{ id: 'fiat-account', label: 'Fiat Account', category: 'assets', icon: '🏦', description: 'Traditional fiat currency account', color: '#22c55e' },
{ id: 'bank-ledger', label: 'Bank Ledger', category: 'assets', icon: '📒', description: 'Core banking ledger entry', color: '#22c55e' },
{ id: 'stablecoin-wallet', label: 'Stablecoin Wallet', category: 'assets', icon: '🪙', description: 'Stablecoin holding wallet', color: '#3b82f6' },
{ id: 'tokenized-security', label: 'Tokenized Security', category: 'assets', icon: '📊', description: 'Tokenized securities instrument', color: '#a855f7' },
{ id: 'commodity-instrument', label: 'Commodity Instrument', category: 'assets', icon: '🛢️', description: 'Physical or digital commodity', color: '#f97316' },
{ id: 'cash-position', label: 'Cash Position', category: 'assets', icon: '💵', description: 'Cash balance position', color: '#22c55e' },
{ id: 'custody-account', label: 'Custody Account', category: 'assets', icon: '🔐', description: 'Custodial holding account', color: '#3b82f6' },
{ id: 'treasury-source', label: 'Treasury Source', category: 'assets', icon: '🏛️', description: 'Treasury management source', color: '#22c55e' },
// Transaction Actions
{ id: 'transfer', label: 'Transfer', category: 'actions', icon: '➡️', description: 'Transfer value between accounts', color: '#3b82f6' },
{ id: 'swap', label: 'Swap', category: 'actions', icon: '🔄', description: 'Swap between asset types', color: '#3b82f6' },
{ id: 'convert', label: 'Convert', category: 'actions', icon: '💱', description: 'FX or asset conversion', color: '#3b82f6' },
{ id: 'split', label: 'Split', category: 'actions', icon: '✂️', description: 'Split value into multiple paths', color: '#3b82f6' },
{ id: 'merge', label: 'Merge', category: 'actions', icon: '🔗', description: 'Merge multiple inputs', color: '#3b82f6' },
{ id: 'lock-unlock', label: 'Lock / Unlock', category: 'actions', icon: '🔒', description: 'Lock or unlock assets', color: '#eab308' },
{ id: 'escrow', label: 'Escrow', category: 'actions', icon: '⏳', description: 'Escrow hold mechanism', color: '#eab308' },
{ id: 'mint-burn', label: 'Mint / Burn', category: 'actions', icon: '🔥', description: 'Mint or burn tokens', color: '#ef4444' },
{ id: 'allocate', label: 'Allocate', category: 'actions', icon: '📤', description: 'Allocate to destinations', color: '#3b82f6' },
{ id: 'rebalance', label: 'Rebalance', category: 'actions', icon: '⚖️', description: 'Portfolio rebalancing', color: '#3b82f6' },
// Routing Components
{ id: 'dex-route', label: 'DEX Route', category: 'routing', icon: '🌐', description: 'Decentralized exchange routing', color: '#a855f7' },
{ id: 'cex-route', label: 'CEX Route', category: 'routing', icon: '🏢', description: 'Centralized exchange routing', color: '#3b82f6' },
{ id: 'otc-desk', label: 'OTC Desk Route', category: 'routing', icon: '🤝', description: 'Over-the-counter desk', color: '#3b82f6' },
{ id: 'banking-rail', label: 'Banking Rail', category: 'routing', icon: '🏦', description: 'Traditional banking rail', color: '#22c55e' },
{ id: 'securities-clearing', label: 'Securities Clearing', category: 'routing', icon: '📋', description: 'Securities clearing route', color: '#a855f7' },
{ id: 'commodity-venue', label: 'Commodity Venue', category: 'routing', icon: '🏭', description: 'Commodity trading venue', color: '#f97316' },
{ id: 'best-execution', label: 'Best Execution Router', category: 'routing', icon: '🎯', description: 'Optimal execution path finder', color: '#22c55e' },
{ id: 'failover-router', label: 'Failover Router', category: 'routing', icon: '🔁', description: 'Fallback routing handler', color: '#eab308' },
// Compliance Components
{ id: 'kyc', label: 'KYC', category: 'compliance', icon: '👤', description: 'Know Your Customer check', color: '#22c55e' },
{ id: 'aml', label: 'AML', category: 'compliance', icon: '🔍', description: 'Anti-Money Laundering screen', color: '#22c55e' },
{ id: 'sanctions', label: 'Sanctions Screening', category: 'compliance', icon: '🚫', description: 'Sanctions list screening', color: '#ef4444' },
{ id: 'jurisdiction', label: 'Jurisdiction Filter', category: 'compliance', icon: '🌍', description: 'Jurisdiction-based filtering', color: '#eab308' },
{ id: 'suitability', label: 'Suitability Check', category: 'compliance', icon: '✅', description: 'Investment suitability assessment', color: '#22c55e' },
{ id: 'travel-rule', label: 'Travel Rule Handler', category: 'compliance', icon: '✈️', description: 'FATF Travel Rule compliance', color: '#3b82f6' },
{ id: 'threshold-alert', label: 'Threshold Alert', category: 'compliance', icon: '⚠️', description: 'Amount threshold monitoring', color: '#eab308' },
{ id: 'approval-gate', label: 'Approval Gate', category: 'compliance', icon: '🚪', description: 'Manual approval checkpoint', color: '#eab308' },
// ISO-20022 / Messaging
{ id: 'pain001', label: 'pain.001', category: 'messaging', icon: '📄', description: 'Customer Credit Transfer Initiation', color: '#a855f7' },
{ id: 'pacs008', label: 'pacs.008', category: 'messaging', icon: '📄', description: 'FI to FI Customer Credit Transfer', color: '#a855f7' },
{ id: 'camt-messages', label: 'camt Messages', category: 'messaging', icon: '📄', description: 'Cash Management messages', color: '#a855f7' },
{ id: 'mapping-transformer', label: 'Mapping Transformer', category: 'messaging', icon: '🔀', description: 'Message format transformer', color: '#a855f7' },
{ id: 'message-validator', label: 'Message Validator', category: 'messaging', icon: '✔️', description: 'ISO-20022 message validation', color: '#a855f7' },
{ id: 'ack-recon', label: 'Ack / Reconciliation', category: 'messaging', icon: '🔄', description: 'Acknowledgement & reconciliation', color: '#a855f7' },
// Logic / Control
{ id: 'if-else', label: 'If / Else', category: 'logic', icon: '🔀', description: 'Conditional branching', color: '#3b82f6' },
{ id: 'branch-jurisdiction', label: 'Branch by Jurisdiction', category: 'logic', icon: '🌍', description: 'Route by legal jurisdiction', color: '#eab308' },
{ id: 'branch-asset', label: 'Branch by Asset Class', category: 'logic', icon: '📊', description: 'Route by asset classification', color: '#3b82f6' },
{ id: 'time-lock', label: 'Time Lock', category: 'logic', icon: '⏰', description: 'Time-based lock condition', color: '#eab308' },
{ id: 'amount-threshold', label: 'Amount Threshold', category: 'logic', icon: '📏', description: 'Amount-based branching', color: '#eab308' },
{ id: 'risk-score', label: 'Risk Score Gate', category: 'logic', icon: '📈', description: 'Risk score evaluation gate', color: '#ef4444' },
{ id: 'manual-approval', label: 'Manual Approval', category: 'logic', icon: '✋', description: 'Human approval step', color: '#eab308' },
{ id: 'retry-policy', label: 'Retry Policy', category: 'logic', icon: '🔁', description: 'Failure retry configuration', color: '#3b82f6' },
// Templates
{ id: 'tpl-cross-border', label: 'Cross-Border Payment', category: 'templates', icon: '🌐', description: 'Multi-jurisdiction payment template', color: '#22c55e' },
{ id: 'tpl-commodity-settlement', label: 'Commodity Settlement', category: 'templates', icon: '🛢️', description: 'Commodity-backed settlement flow', color: '#f97316' },
{ id: 'tpl-stablecoin-offramp', label: 'Stablecoin Off-Ramp', category: 'templates', icon: '🪙', description: 'Stablecoin to fiat off-ramp', color: '#3b82f6' },
{ id: 'tpl-securities-collateral', label: 'Securities Collateral', category: 'templates', icon: '📊', description: 'Securities collateral transfer', color: '#a855f7' },
{ id: 'tpl-treasury-rebalance', label: 'Treasury Rebalancing', category: 'templates', icon: '⚖️', description: 'Treasury position rebalancing', color: '#22c55e' },
{ id: 'tpl-custody-movement', label: 'Custody Movement', category: 'templates', icon: '🔐', description: 'Institutional custody transfer', color: '#3b82f6' },
];

138
src/data/portalData.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { Account, FinancialSummary, TreasuryPosition, CashForecast, ReportConfig, ComplianceAlert, SettlementRecord, PortalModule } from '../types/portal';
export const portalModules: PortalModule[] = [
{ id: 'dashboard', name: 'Overview', icon: '📊', description: 'Consolidated financial dashboard with real-time portfolio metrics', path: '/dashboard', requiredPermission: 'accounts.view', status: 'active' },
{ id: 'transaction-builder', name: 'Transaction Builder', icon: '⚡', description: 'IDE-style drag-and-drop transaction composition workspace', path: '/transaction-builder', requiredPermission: 'transactions.create', status: 'active' },
{ id: 'accounts', name: 'Accounts', icon: '🏦', description: 'Multi-account and subaccount management with consolidated views', path: '/accounts', requiredPermission: 'accounts.view', status: 'active' },
{ id: 'treasury', name: 'Treasury', icon: '💎', description: 'Treasury operations, cash management, and position monitoring', path: '/treasury', requiredPermission: 'treasury.view', status: 'active' },
{ id: 'reporting', name: 'Reporting', icon: '📋', description: 'IPSAS, US GAAP, and IFRS compliant financial reporting', path: '/reporting', requiredPermission: 'reports.view', status: 'active' },
{ id: 'compliance', name: 'Compliance & Risk', icon: '🛡️', description: 'Regulatory compliance monitoring and risk management', path: '/compliance', requiredPermission: 'compliance.view', status: 'active' },
{ id: 'settlements', name: 'Settlements', icon: '✅', description: 'Settlement lifecycle tracking and clearing operations', path: '/settlements', requiredPermission: 'settlements.view', status: 'active' },
];
export const sampleAccounts: Account[] = [
{
id: 'acc-001', name: 'Main Operating Account', type: 'operating', currency: 'USD',
balance: 45_250_000.00, availableBalance: 44_800_000.00, status: 'active',
institution: 'Solace Bank Group PLC', iban: 'GB82 SLCE 0099 7100 0012 34',
swift: 'SLCEGB2L', lastActivity: new Date(Date.now() - 300000),
subaccounts: [
{ id: 'acc-001a', name: 'Payroll Sub-Account', type: 'operating', currency: 'USD', balance: 2_100_000, availableBalance: 2_100_000, status: 'active', parentId: 'acc-001', institution: 'Solace Bank Group PLC', lastActivity: new Date(Date.now() - 600000) },
{ id: 'acc-001b', name: 'Vendor Payments', type: 'operating', currency: 'USD', balance: 3_500_000, availableBalance: 3_200_000, status: 'active', parentId: 'acc-001', institution: 'Solace Bank Group PLC', lastActivity: new Date(Date.now() - 900000) },
],
},
{
id: 'acc-002', name: 'EUR Treasury Account', type: 'treasury', currency: 'EUR',
balance: 18_750_000.00, availableBalance: 18_500_000.00, status: 'active',
institution: 'Solace Bank Group PLC', iban: 'GB45 SLCE 0099 7200 0056 78',
swift: 'SLCEGB2L', lastActivity: new Date(Date.now() - 1200000),
},
{
id: 'acc-003', name: 'Digital Asset Custody', type: 'custody', currency: 'BTC',
balance: 125.5, availableBalance: 120.0, status: 'active',
institution: 'Solace Bank Group PLC', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD38',
lastActivity: new Date(Date.now() - 1800000),
},
{
id: 'acc-004', name: 'Stablecoin Reserve', type: 'stablecoin', currency: 'USDC',
balance: 12_000_000.00, availableBalance: 11_950_000.00, status: 'active',
institution: 'Solace Bank Group PLC', walletAddress: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
lastActivity: new Date(Date.now() - 2400000),
},
{
id: 'acc-005', name: 'Nostro - Deutsche Bank', type: 'nostro', currency: 'EUR',
balance: 5_200_000.00, availableBalance: 5_200_000.00, status: 'active',
institution: 'Deutsche Bank AG', swift: 'DEUTDEFF',
lastActivity: new Date(Date.now() - 3600000),
},
{
id: 'acc-006', name: 'Collateral Account', type: 'collateral', currency: 'USD',
balance: 8_000_000.00, availableBalance: 0, status: 'active',
institution: 'Solace Bank Group PLC', lastActivity: new Date(Date.now() - 7200000),
},
{
id: 'acc-007', name: 'GBP Settlement Account', type: 'settlement', currency: 'GBP',
balance: 3_400_000.00, availableBalance: 3_150_000.00, status: 'active',
institution: 'Solace Bank Group PLC', iban: 'GB12 SLCE 0099 7300 0098 76',
swift: 'SLCEGB2L', lastActivity: new Date(Date.now() - 5400000),
},
{
id: 'acc-008', name: 'Escrow - Project Alpha', type: 'escrow', currency: 'USD',
balance: 15_000_000.00, availableBalance: 0, status: 'frozen',
institution: 'Solace Bank Group PLC', lastActivity: new Date(Date.now() - 86400000),
},
];
export const financialSummary: FinancialSummary = {
totalAssets: 892_450_000,
totalLiabilities: 654_200_000,
netPosition: 238_250_000,
unrealizedPnL: 4_125_000,
realizedPnL: 12_680_000,
pendingSettlements: 28_500_000,
dailyVolume: 156_000_000,
currency: 'USD',
};
export const treasuryPositions: TreasuryPosition[] = [
{ id: 'pos-1', assetClass: 'Fixed Income', instrument: 'US Treasury 10Y', quantity: 50_000_000, marketValue: 49_250_000, costBasis: 48_500_000, unrealizedPnL: 750_000, currency: 'USD', custodian: 'State Street', maturityDate: new Date('2034-11-15') },
{ id: 'pos-2', assetClass: 'Fixed Income', instrument: 'UK Gilt 5Y', quantity: 20_000_000, marketValue: 19_800_000, costBasis: 20_100_000, unrealizedPnL: -300_000, currency: 'GBP', custodian: 'Euroclear' },
{ id: 'pos-3', assetClass: 'Digital Assets', instrument: 'Bitcoin (BTC)', quantity: 125.5, marketValue: 8_425_000, costBasis: 6_275_000, unrealizedPnL: 2_150_000, currency: 'USD', custodian: 'BitGo' },
{ id: 'pos-4', assetClass: 'Digital Assets', instrument: 'USDC Stablecoin', quantity: 12_000_000, marketValue: 12_000_000, costBasis: 12_000_000, unrealizedPnL: 0, currency: 'USD', custodian: 'Circle' },
{ id: 'pos-5', assetClass: 'FX', instrument: 'EUR/USD Spot', quantity: 18_750_000, marketValue: 20_250_000, costBasis: 19_875_000, unrealizedPnL: 375_000, currency: 'USD', custodian: 'Solace Bank' },
{ id: 'pos-6', assetClass: 'Commodities', instrument: 'Gold (XAU)', quantity: 5_000, marketValue: 11_500_000, costBasis: 9_750_000, unrealizedPnL: 1_750_000, currency: 'USD', custodian: 'HSBC Vault' },
{ id: 'pos-7', assetClass: 'Equities', instrument: 'S&P 500 ETF', quantity: 100_000, marketValue: 45_200_000, costBasis: 42_000_000, unrealizedPnL: 3_200_000, currency: 'USD', custodian: 'State Street' },
{ id: 'pos-8', assetClass: 'Fixed Income', instrument: 'Corporate Bond AAA', quantity: 15_000_000, marketValue: 14_850_000, costBasis: 15_000_000, unrealizedPnL: -150_000, currency: 'USD', custodian: 'JP Morgan', maturityDate: new Date('2028-06-30') },
];
export const cashForecasts: CashForecast[] = Array.from({ length: 30 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() + i);
const base = 45_250_000 + Math.sin(i * 0.3) * 5_000_000;
return {
date,
projected: Math.round(base + (Math.random() - 0.5) * 2_000_000),
actual: i < 3 ? Math.round(base + (Math.random() - 0.5) * 1_000_000) : undefined,
currency: 'USD',
};
});
export const reportConfigs: ReportConfig[] = [
{ id: 'rpt-1', name: 'Balance Sheet - IFRS', standard: 'IFRS', type: 'balance_sheet', period: 'quarterly', status: 'published', generatedAt: new Date(Date.now() - 86400000 * 5), generatedBy: 'J. Thompson' },
{ id: 'rpt-2', name: 'Income Statement - US GAAP', standard: 'US_GAAP', type: 'income_statement', period: 'monthly', status: 'reviewed', generatedAt: new Date(Date.now() - 86400000 * 2), generatedBy: 'M. Chen' },
{ id: 'rpt-3', name: 'Cash Flow Statement - IPSAS', standard: 'IPSAS', type: 'cash_flow', period: 'quarterly', status: 'generated', generatedAt: new Date(Date.now() - 86400000), generatedBy: 'System' },
{ id: 'rpt-4', name: 'Trial Balance - IFRS', standard: 'IFRS', type: 'trial_balance', period: 'monthly', status: 'published', generatedAt: new Date(Date.now() - 86400000 * 3), generatedBy: 'A. Patel' },
{ id: 'rpt-5', name: 'Regulatory Report - US GAAP', standard: 'US_GAAP', type: 'regulatory', period: 'quarterly', status: 'draft', generatedBy: 'System' },
{ id: 'rpt-6', name: 'Position Summary - IFRS', standard: 'IFRS', type: 'position_summary', period: 'daily', status: 'published', generatedAt: new Date(Date.now() - 3600000), generatedBy: 'System' },
{ id: 'rpt-7', name: 'Risk Exposure - IPSAS', standard: 'IPSAS', type: 'risk_exposure', period: 'weekly', status: 'generated', generatedAt: new Date(Date.now() - 86400000 * 1), generatedBy: 'R. Kumar' },
{ id: 'rpt-8', name: 'Compliance Summary - US GAAP', standard: 'US_GAAP', type: 'compliance_summary', period: 'monthly', status: 'reviewed', generatedAt: new Date(Date.now() - 86400000 * 4), generatedBy: 'L. Wright' },
];
export const complianceAlerts: ComplianceAlert[] = [
{ id: 'ca-1', severity: 'critical', category: 'AML', message: 'Unusual transaction pattern detected on ACC-001: 15 transactions exceeding $500K in 24h', timestamp: new Date(Date.now() - 1800000), status: 'open' },
{ id: 'ca-2', severity: 'high', category: 'KYC', message: 'KYC documentation expiring for 3 institutional counterparties within 30 days', timestamp: new Date(Date.now() - 3600000), status: 'acknowledged', assignedTo: 'Compliance Team' },
{ id: 'ca-3', severity: 'medium', category: 'Sanctions', message: 'New OFAC SDN list update — 12 new entries require screening', timestamp: new Date(Date.now() - 7200000), status: 'open' },
{ id: 'ca-4', severity: 'high', category: 'Travel Rule', message: 'Travel rule compliance gap: 2 outbound transfers missing originator data', timestamp: new Date(Date.now() - 10800000), status: 'open' },
{ id: 'ca-5', severity: 'low', category: 'Reporting', message: 'Q4 IPSAS regulatory filing due in 14 days', timestamp: new Date(Date.now() - 14400000), status: 'acknowledged', assignedTo: 'Finance Team' },
{ id: 'ca-6', severity: 'medium', category: 'Risk', message: 'Counterparty credit rating downgrade: Acme Corp (BBB → BB+)', timestamp: new Date(Date.now() - 21600000), status: 'resolved' },
];
export const settlementRecords: SettlementRecord[] = [
{ id: 'stl-1', txId: 'TX-2024-0851', type: 'DVP', status: 'pending', amount: 5_000_000, currency: 'USD', counterparty: 'Goldman Sachs', settlementDate: new Date(Date.now() + 86400000), valueDate: new Date(Date.now() + 86400000), csd: 'DTCC' },
{ id: 'stl-2', txId: 'TX-2024-0852', type: 'PVP', status: 'matched', amount: 12_500_000, currency: 'EUR', counterparty: 'Deutsche Bank', settlementDate: new Date(Date.now() + 172800000), valueDate: new Date(Date.now() + 172800000), csd: 'Euroclear' },
{ id: 'stl-3', txId: 'TX-2024-0853', type: 'FOP', status: 'affirmed', amount: 2_000_000, currency: 'GBP', counterparty: 'Barclays', settlementDate: new Date(), valueDate: new Date(), csd: 'CREST' },
{ id: 'stl-4', txId: 'TX-2024-0854', type: 'internal', status: 'settled', amount: 8_000_000, currency: 'USD', counterparty: 'Internal Transfer', settlementDate: new Date(Date.now() - 86400000), valueDate: new Date(Date.now() - 86400000) },
{ id: 'stl-5', txId: 'TX-2024-0855', type: 'DVP', status: 'failed', amount: 3_250_000, currency: 'USD', counterparty: 'Morgan Stanley', settlementDate: new Date(Date.now() - 172800000), valueDate: new Date(Date.now() - 172800000), csd: 'DTCC' },
{ id: 'stl-6', txId: 'TX-2024-0856', type: 'PVP', status: 'pending', amount: 15_000_000, currency: 'JPY', counterparty: 'Nomura', settlementDate: new Date(Date.now() + 259200000), valueDate: new Date(Date.now() + 259200000) },
];
export const recentActivity = [
{ id: 'ra-1', action: 'Transfer Executed', detail: '$2.5M USD → EUR Treasury Account', timestamp: new Date(Date.now() - 300000), status: 'success' as const },
{ id: 'ra-2', action: 'Settlement Confirmed', detail: 'TX-2024-0847 settled via SWIFT', timestamp: new Date(Date.now() - 1200000), status: 'success' as const },
{ id: 'ra-3', action: 'Compliance Alert', detail: 'AML threshold exceeded on ACC-001', timestamp: new Date(Date.now() - 1800000), status: 'warning' as const },
{ id: 'ra-4', action: 'Report Generated', detail: 'Q4 Balance Sheet (IFRS)', timestamp: new Date(Date.now() - 3600000), status: 'info' as const },
{ id: 'ra-5', action: 'Position Rebalanced', detail: 'Treasury portfolio rebalanced per policy', timestamp: new Date(Date.now() - 5400000), status: 'success' as const },
{ id: 'ra-6', action: 'Settlement Failed', detail: 'TX-2024-0855 DVP failed — counterparty mismatch', timestamp: new Date(Date.now() - 7200000), status: 'error' as const },
{ id: 'ra-7', action: 'New Account Created', detail: 'GBP Settlement Account activated', timestamp: new Date(Date.now() - 10800000), status: 'info' as const },
{ id: 'ra-8', action: 'KYC Review', detail: 'Counterparty due diligence completed for Barclays', timestamp: new Date(Date.now() - 14400000), status: 'success' as const },
];

88
src/data/sampleData.ts Normal file
View File

@@ -0,0 +1,88 @@
import type { ChatMessage, TerminalEntry, ValidationIssue, AuditEntry, SettlementItem, Notification, ThreadEntry } from '../types';
export const sampleMessages: ChatMessage[] = [
{
id: '1',
agent: 'Builder',
content: 'Transaction graph initialized. Drop components from the left panel to begin building your flow.',
timestamp: new Date(Date.now() - 300000),
type: 'agent',
},
{
id: '2',
agent: 'Compliance',
content: 'Compliance engine ready. I\'ll monitor your graph for policy violations as you build.',
timestamp: new Date(Date.now() - 240000),
type: 'agent',
},
{
id: '3',
agent: 'System',
content: 'Environment: Sandbox | Region: Multi-jurisdiction | Protocol: ISO-20022 enabled',
timestamp: new Date(Date.now() - 180000),
type: 'system',
},
];
export const sampleTerminal: TerminalEntry[] = [
{ id: '1', timestamp: new Date(Date.now() - 60000), level: 'info', source: 'system', message: 'Transaction builder initialized' },
{ id: '2', timestamp: new Date(Date.now() - 55000), level: 'info', source: 'compliance', message: 'Compliance engine v3.2.1 loaded' },
{ id: '3', timestamp: new Date(Date.now() - 50000), level: 'success', source: 'routing', message: 'Route optimizer connected to 12 venues' },
{ id: '4', timestamp: new Date(Date.now() - 45000), level: 'info', source: 'iso20022', message: 'Message schemas loaded: pain.001, pacs.008, camt.053' },
{ id: '5', timestamp: new Date(Date.now() - 40000), level: 'warn', source: 'market', message: 'EUR/USD spread widened to 2.3bps' },
{ id: '6', timestamp: new Date(Date.now() - 30000), level: 'info', source: 'system', message: 'Sandbox environment ready' },
];
export const sampleValidation: ValidationIssue[] = [
{ id: '1', severity: 'info', message: 'Graph contains 0 nodes. Add components to begin validation.' },
];
export const sampleAudit: AuditEntry[] = [
{ id: '1', timestamp: new Date(Date.now() - 120000), user: 'system', action: 'SESSION_START', detail: 'Sandbox session initialized' },
{ id: '2', timestamp: new Date(Date.now() - 110000), user: 'system', action: 'ENGINE_LOAD', detail: 'Compliance matrices loaded (47 rules)' },
{ id: '3', timestamp: new Date(Date.now() - 100000), user: 'system', action: 'ENGINE_LOAD', detail: 'Routing engine initialized with 12 venues' },
];
export const sampleSettlement: SettlementItem[] = [
{ id: '1', txId: 'TX-2024-0847', status: 'settled', amount: '1,250,000.00', asset: 'USD', counterparty: 'Acme Corp', timestamp: new Date(Date.now() - 3600000) },
{ id: '2', txId: 'TX-2024-0848', status: 'pending', amount: '500,000.00', asset: 'EUR', counterparty: 'Deutsche Bank', timestamp: new Date(Date.now() - 1800000) },
{ id: '3', txId: 'TX-2024-0849', status: 'in_review', amount: '2,000.00', asset: 'BTC', counterparty: 'BitGo Custody', timestamp: new Date(Date.now() - 900000) },
{ id: '4', txId: 'TX-2024-0850', status: 'awaiting_approval', amount: '750,000.00', asset: 'GBP', counterparty: 'Barclays', timestamp: new Date(Date.now() - 600000) },
];
export const sampleNotifications: Notification[] = [
{ id: '1', title: 'Compliance Update', message: 'New FATF travel rule requirements effective in your jurisdiction', type: 'warning', timestamp: new Date(Date.now() - 600000), read: false },
{ id: '2', title: 'Route Optimization', message: 'New liquidity venue added: Coinbase Prime', type: 'info', timestamp: new Date(Date.now() - 1200000), read: false },
{ id: '3', title: 'Settlement Complete', message: 'TX-2024-0847 settled successfully via SWIFT', type: 'success', timestamp: new Date(Date.now() - 3600000), read: true },
];
export const sampleThreads: ThreadEntry[] = [
{ id: 'thread-1', title: 'Cross-border payment setup', agent: 'Builder', timestamp: new Date(Date.now() - 86400000), messageCount: 12 },
{ id: 'thread-2', title: 'AML compliance review', agent: 'Compliance', timestamp: new Date(Date.now() - 172800000), messageCount: 8 },
{ id: 'thread-3', title: 'SWIFT message generation', agent: 'ISO-20022', timestamp: new Date(Date.now() - 259200000), messageCount: 5 },
];
export const sampleReconciliation = [
{ id: '1', txId: 'TX-2024-0847', internalRef: 'INT-00847', externalRef: 'EXT-SW-4821', status: 'matched', amount: '1,250,000.00', asset: 'USD', timestamp: new Date(Date.now() - 3600000) },
{ id: '2', txId: 'TX-2024-0845', internalRef: 'INT-00845', externalRef: 'EXT-SW-4819', status: 'unmatched', amount: '320,000.00', asset: 'EUR', timestamp: new Date(Date.now() - 7200000) },
{ id: '3', txId: 'TX-2024-0843', internalRef: 'INT-00843', externalRef: 'EXT-CB-1102', status: 'matched', amount: '15.5', asset: 'BTC', timestamp: new Date(Date.now() - 10800000) },
];
export const sampleExceptions = [
{ id: '1', txId: 'TX-2024-0846', type: 'timeout', message: 'Settlement acknowledgement not received within SLA (T+2)', severity: 'error' as const, timestamp: new Date(Date.now() - 5400000) },
{ id: '2', txId: 'TX-2024-0844', type: 'mismatch', message: 'Amount mismatch: expected 500,000.00 EUR, received 499,998.50 EUR', severity: 'warning' as const, timestamp: new Date(Date.now() - 9000000) },
{ id: '3', txId: 'TX-2024-0842', type: 'rejected', message: 'Counterparty rejected: sanctions screening flag on beneficiary', severity: 'error' as const, timestamp: new Date(Date.now() - 14400000) },
];
export const sampleMessageQueue = [
{ id: '1', msgType: 'pain.001', direction: 'outbound' as const, counterparty: 'Deutsche Bank', status: 'sent', timestamp: new Date(Date.now() - 1800000) },
{ id: '2', msgType: 'pacs.008', direction: 'inbound' as const, counterparty: 'Barclays', status: 'received', timestamp: new Date(Date.now() - 2400000) },
{ id: '3', msgType: 'camt.053', direction: 'inbound' as const, counterparty: 'SWIFT', status: 'processing', timestamp: new Date(Date.now() - 3000000) },
];
export const sampleEvents = [
{ id: '1', type: 'NODE_ADDED', detail: 'Fiat Account node added to canvas', timestamp: new Date(Date.now() - 60000) },
{ id: '2', type: 'EDGE_CREATED', detail: 'Connection established: Fiat Account → Transfer', timestamp: new Date(Date.now() - 55000) },
{ id: '3', type: 'VALIDATION_RUN', detail: 'Graph validation completed — 0 errors, 1 warning', timestamp: new Date(Date.now() - 50000) },
{ id: '4', type: 'AGENT_INVOKED', detail: 'Builder Agent queried for routing suggestion', timestamp: new Date(Date.now() - 45000) },
];

3853
src/index.css Normal file

File diff suppressed because it is too large Load Diff

16
src/main.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import './index.css'
import Portal from './Portal'
import { AuthProvider } from './contexts/AuthContext'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<HashRouter>
<AuthProvider>
<Portal />
</AuthProvider>
</HashRouter>
</StrictMode>,
)

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

108
src/types/index.ts Normal file
View File

@@ -0,0 +1,108 @@
import type { Node, Edge } from '@xyflow/react';
export interface ComponentItem {
id: string;
label: string;
category: string;
icon: string;
description: string;
color: string;
inputs?: string[];
outputs?: string[];
engines?: string[];
}
export interface ChatMessage {
id: string;
agent: string;
content: string;
timestamp: Date;
type: 'user' | 'agent' | 'system';
}
export interface TerminalEntry {
id: string;
timestamp: Date;
level: 'info' | 'warn' | 'error' | 'success';
source: string;
message: string;
}
export interface ValidationIssue {
id: string;
severity: 'error' | 'warning' | 'info';
node?: string;
field?: string;
message: string;
}
export interface AuditEntry {
id: string;
timestamp: Date;
user: string;
action: string;
detail: string;
}
export interface SettlementItem {
id: string;
txId: string;
status: 'pending' | 'in_review' | 'awaiting_approval' | 'dispatched' | 'partially_settled' | 'settled' | 'failed';
amount: string;
asset: string;
counterparty: string;
timestamp: Date;
}
export interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error';
timestamp: Date;
read: boolean;
}
export interface ThreadEntry {
id: string;
title: string;
agent: Agent;
timestamp: Date;
messageCount: number;
}
export interface TransactionTab {
id: string;
name: string;
nodes: Node[];
edges: Edge[];
}
export interface HistoryEntry {
nodes: Node[];
edges: Edge[];
}
export type TransactionNode = Node<{
label: string;
category: string;
icon: string;
color: string;
status?: 'valid' | 'warning' | 'error';
}>;
export type TransactionEdge = Edge<{
animated?: boolean;
}>;
export type PanelSide = 'left' | 'right' | 'bottom';
export type SessionMode = 'Sandbox' | 'Simulate' | 'Live' | 'Compliance Review';
export type ActivityTab = 'builder' | 'assets' | 'templates' | 'compliance' | 'routes' | 'protocols' | 'agents' | 'terminal' | 'audit' | 'settings';
export type BottomTab = 'terminal' | 'validation' | '800system' | 'settlement' | 'audit' | 'messages' | 'events' | 'reconciliation' | 'exceptions';
export type Agent = 'Builder' | 'Compliance' | 'Routing' | 'ISO-20022' | 'Settlement' | 'Risk' | 'Documentation';
export type ConversationScope = 'current-node' | 'current-flow' | 'full-transaction' | 'terminal' | 'compliance';

143
src/types/portal.ts Normal file
View File

@@ -0,0 +1,143 @@
export interface WalletInfo {
address: string;
chainId: number;
balance: string;
ensName?: string;
provider: 'metamask' | 'walletconnect' | 'coinbase' | 'injected';
}
export interface AuthState {
isAuthenticated: boolean;
wallet: WalletInfo | null;
user: PortalUser | null;
loading: boolean;
}
export interface PortalUser {
id: string;
displayName: string;
role: UserRole;
permissions: Permission[];
institution: string;
department: string;
lastLogin: Date;
walletAddress: string;
}
export type UserRole = 'admin' | 'treasurer' | 'analyst' | 'compliance_officer' | 'auditor' | 'viewer';
export type Permission =
| 'accounts.view' | 'accounts.manage' | 'accounts.create'
| 'transactions.view' | 'transactions.create' | 'transactions.approve' | 'transactions.execute'
| 'treasury.view' | 'treasury.manage' | 'treasury.rebalance'
| 'compliance.view' | 'compliance.manage' | 'compliance.override'
| 'reports.view' | 'reports.generate' | 'reports.export'
| 'settlements.view' | 'settlements.approve'
| 'admin.users' | 'admin.settings' | 'admin.audit';
export interface Account {
id: string;
name: string;
type: AccountType;
currency: string;
balance: number;
availableBalance: number;
status: 'active' | 'frozen' | 'closed' | 'pending';
parentId?: string;
institution: string;
iban?: string;
swift?: string;
walletAddress?: string;
lastActivity: Date;
subaccounts?: Account[];
}
export type AccountType =
| 'operating' | 'reserve' | 'custody' | 'escrow'
| 'settlement' | 'nostro' | 'vostro' | 'collateral'
| 'treasury' | 'crypto_wallet' | 'stablecoin' | 'omnibus';
export interface FinancialSummary {
totalAssets: number;
totalLiabilities: number;
netPosition: number;
unrealizedPnL: number;
realizedPnL: number;
pendingSettlements: number;
dailyVolume: number;
currency: string;
}
export interface TreasuryPosition {
id: string;
assetClass: string;
instrument: string;
quantity: number;
marketValue: number;
costBasis: number;
unrealizedPnL: number;
currency: string;
custodian: string;
maturityDate?: Date;
}
export interface CashForecast {
date: Date;
projected: number;
actual?: number;
variance?: number;
currency: string;
}
export type ReportingStandard = 'IPSAS' | 'US_GAAP' | 'IFRS';
export interface ReportConfig {
id: string;
name: string;
standard: ReportingStandard;
type: ReportType;
period: ReportPeriod;
status: 'draft' | 'generated' | 'reviewed' | 'published';
generatedAt?: Date;
generatedBy?: string;
}
export type ReportType =
| 'balance_sheet' | 'income_statement' | 'cash_flow'
| 'trial_balance' | 'general_ledger' | 'regulatory'
| 'position_summary' | 'risk_exposure' | 'compliance_summary';
export type ReportPeriod = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'annual' | 'custom';
export interface PortalModule {
id: string;
name: string;
icon: string;
description: string;
path: string;
requiredPermission: Permission;
status: 'active' | 'coming_soon' | 'maintenance';
}
export interface ComplianceAlert {
id: string;
severity: 'critical' | 'high' | 'medium' | 'low';
category: string;
message: string;
timestamp: Date;
status: 'open' | 'acknowledged' | 'resolved';
assignedTo?: string;
}
export interface SettlementRecord {
id: string;
txId: string;
type: 'DVP' | 'FOP' | 'PVP' | 'internal';
status: 'pending' | 'matched' | 'affirmed' | 'settled' | 'failed' | 'cancelled';
amount: number;
currency: string;
counterparty: string;
settlementDate: Date;
valueDate: Date;
csd?: string;
}

25
tsconfig.app.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})