aibanking.dev / views /VirtualAccounts.tsx
admin08077's picture
Update views/VirtualAccounts.tsx
c507e6d verified
import React, { useState, useMemo } from 'react';
import {
Network,
Layers,
Search,
RefreshCw,
ArrowUpRight,
Building,
ShieldCheck,
Plus,
Trash2,
ChevronRight,
MoreVertical,
X,
Loader2,
Terminal,
Activity,
Settings2,
CheckCircle2,
Cpu,
AlertCircle
} from 'lucide-react';
import { VirtualAccount } from '../types/index.ts';
import { callGemini } from '../services/geminiService.ts';
const INITIAL_MOCK_VA: VirtualAccount[] = [
{
id: 'VA-88021',
object: 'virtual_account',
live_mode: true,
created_at: '2024-02-15T10:00:00Z',
updated_at: '2024-03-31T14:20:00Z',
name: 'Q1 Sales Collection - UK',
description: 'Virtual account for routing Sterling sales to main treasury.',
counterparty_id: 'CP-8801',
internal_account_id: 'IA-4401',
account_details: [],
routing_details: [],
status: 'ACTIVE'
},
{
id: 'VA-90112',
object: 'virtual_account',
live_mode: true,
created_at: '2024-03-01T09:30:00Z',
updated_at: '2024-03-31T12:00:00Z',
name: 'EMEA Vendor Pass-through',
description: 'Temporary buffer for Eurozone contractor payments.',
counterparty_id: null,
internal_account_id: 'IA-9902',
account_details: [],
routing_details: [],
status: 'ACTIVE'
},
{
id: 'VA-11234',
object: 'virtual_account',
live_mode: true,
created_at: '2023-12-10T11:45:00Z',
updated_at: '2024-03-25T16:00:00Z',
name: 'Legacy APAC Bridge',
description: 'Deprecated node being phased out.',
counterparty_id: 'CP-1105',
internal_account_id: 'IA-4401',
account_details: [],
routing_details: [],
status: 'PENDING'
}
];
const VirtualAccounts: React.FC = () => {
const [loading, setLoading] = useState(false);
const [accounts, setAccounts] = useState<VirtualAccount[]>(INITIAL_MOCK_VA);
// Modal States
const [isProvisionOpen, setIsProvisionOpen] = useState(false);
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [isReconcilerOpen, setIsReconcilerOpen] = useState(false);
const [selectedVa, setSelectedVa] = useState<VirtualAccount | null>(null);
// Provisioning State
const [provisioning, setProvisioning] = useState(false);
const [provisionError, setProvisionError] = useState<string | null>(null);
const [newVa, setNewVa] = useState({ name: '', description: '', parent: 'IA-4401' });
// Reconciler State
const [reconciling, setReconciling] = useState(false);
const [reconcileReport, setReconcileReport] = useState<string | null>(null);
const refreshNodes = () => {
setLoading(true);
setTimeout(() => setLoading(false), 1200);
};
const handleProvision = async () => {
setProvisionError(null);
const trimmedName = newVa.name.trim();
// 1. Client-Side Parity Check: Duplicate Names
if (accounts.some(acc => acc.name.toLowerCase() === trimmedName.toLowerCase())) {
setProvisionError("Node identity collision: A node with this designation already exists in the virtual mesh.");
return;
}
setProvisioning(true);
try {
// 2. Artificial neural network handshake simulation
await new Promise(r => setTimeout(r, 2000));
// 3. Simulated Registry-Level Validation (e.g. invalid parent or system refusal)
if (trimmedName.toLowerCase() === 'fail') {
throw new Error("Registry Rejection: Parent Account ID " + newVa.parent + " refused handshake authorization.");
}
const node: VirtualAccount = {
id: `VA-${Math.floor(10000 + Math.random() * 90000)}`,
object: 'virtual_account',
live_mode: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
name: trimmedName || 'Untitled Node',
description: newVa.description || 'Provisioned via Quantum Foundry',
counterparty_id: null,
internal_account_id: newVa.parent,
account_details: [],
routing_details: [],
status: 'ACTIVE'
};
setAccounts([node, ...accounts]);
setIsProvisionOpen(false);
setNewVa({ name: '', description: '', parent: 'IA-4401' });
} catch (err: any) {
setProvisionError(err.message || "Protocol Handshake Failure: The global registry node timed out.");
} finally {
setProvisioning(false);
}
};
const handleRunReconciler = async () => {
setReconciling(true);
setReconcileReport(null);
try {
const response = await callGemini(
'gemini-3-flash-preview',
"Generate a professional 2-sentence summary for a financial reconciliation process. Mention 99.9% match accuracy and the resolution of 12 incoming virtual node signals across UK and EMEA regions. Tone: Technical and secure."
);
// Fix: Access .text helper directly from the SDK response object
setReconcileReport(response.text);
} catch (err) {
setReconcileReport("Neural reconciliation complete. Signal integrity verified at 99.9%. All ledger entries matched with zero drift.");
} finally {
setReconciling(false);
}
};
const deleteNode = (id: string) => {
if (confirm("De-provision node? This will sever all incoming signal routing.")) {
setAccounts(accounts.filter(a => a.id !== id));
}
};
const openProvisionModal = () => {
setProvisionError(null);
setIsProvisionOpen(true);
};
return (
<div className="space-y-10 animate-in fade-in duration-700">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div>
<h2 className="text-3xl font-black text-white italic tracking-tighter uppercase mb-2">Virtual <span className="text-blue-500 not-italic">Nodes</span></h2>
<p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.3em]">High-Volume Disbursement & Collection Layer</p>
</div>
<div className="flex gap-4">
<button
onClick={refreshNodes}
className="flex items-center space-x-2 px-6 py-3 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all"
>
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
<span>Poll Virtual Mesh</span>
</button>
<button
onClick={openProvisionModal}
className="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all shadow-xl shadow-blue-900/40"
>
<Plus size={14} />
<span>Provision Virtual Account</span>
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
{accounts.map(va => (
<div key={va.id} className="bg-zinc-950 border border-zinc-900 rounded-[3rem] p-10 flex flex-col justify-between group hover:border-blue-500/30 transition-all relative overflow-hidden shadow-2xl">
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
<Layers size={120} />
</div>
<div className="relative z-10">
<div className="flex justify-between items-start mb-10">
<div className="w-16 h-16 bg-zinc-900 rounded-3xl flex items-center justify-center border border-zinc-800 text-blue-500 shadow-xl group-hover:scale-110 transition-transform duration-500">
<Network size={32} />
</div>
<div className={`px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest ${
va.status === 'ACTIVE' ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-500' : 'bg-amber-500/10 border-amber-500/20 text-amber-500'
}`}>
{va.status}
</div>
</div>
<h3 className="text-xl font-black text-white italic tracking-tighter uppercase mb-2 truncate pr-10">{va.name}</h3>
<p className="text-zinc-500 text-xs leading-relaxed mb-8 h-12 overflow-hidden line-clamp-2">{va.description}</p>
<div className="space-y-4 mb-10">
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.2em] text-zinc-600">
<span>Parent Registry</span>
<span className="text-zinc-300 mono">{va.internal_account_id}</span>
</div>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.2em] text-zinc-600">
<span>ID Segment</span>
<span className="text-zinc-300 mono">{va.id}</span>
</div>
<div className="flex justify-between items-center text-[10px] font-black uppercase tracking-[0.2em] text-zinc-600">
<span>Routing Layer</span>
<span className="text-blue-500 mono">FDX_V6_MESH</span>
</div>
</div>
</div>
<div className="flex gap-4 relative z-10">
<button
onClick={() => {
setSelectedVa(va);
setIsConfigOpen(true);
}}
className="flex-1 py-4 bg-zinc-900 hover:bg-zinc-800 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all border border-zinc-800 flex items-center justify-center gap-2"
>
<Settings2 size={14} />
Config
</button>
<button
onClick={() => deleteNode(va.id)}
className="p-4 bg-zinc-900 hover:bg-rose-500/10 text-zinc-600 hover:text-rose-500 rounded-2xl transition-all border border-zinc-800"
>
<Trash2 size={18} />
</button>
</div>
</div>
))}
<button
onClick={openProvisionModal}
className="border-2 border-dashed border-zinc-900 rounded-[3rem] p-10 flex flex-col items-center justify-center group hover:border-zinc-700 transition-all gap-4 bg-black/20"
>
<div className="w-16 h-16 bg-zinc-950 rounded-full flex items-center justify-center text-zinc-800 group-hover:text-blue-500 transition-colors shadow-2xl">
<Plus size={32} />
</div>
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-zinc-700 group-hover:text-zinc-400">Initialize Node</span>
</button>
</div>
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] p-10 relative overflow-hidden group shadow-2xl">
<div className="flex flex-col md:flex-row justify-between items-center gap-10">
<div className="flex items-center gap-8">
<div className="w-20 h-20 bg-blue-600/10 rounded-3xl flex items-center justify-center text-blue-500 shadow-2xl shadow-blue-500/5">
<ShieldCheck size={40} />
</div>
<div>
<h4 className="text-xl font-black text-white italic tracking-tighter uppercase mb-2">Automated <span className="text-blue-500 not-italic">Reconciliation</span></h4>
<p className="text-zinc-500 text-xs max-w-lg leading-relaxed font-medium">Lumina Quantum Oracle automatically matches incoming virtual node signals to your internal accounting ledger with 99.9% precision.</p>
</div>
</div>
<button
onClick={() => {
setIsReconcilerOpen(true);
handleRunReconciler();
}}
className="px-10 py-5 bg-white hover:bg-zinc-200 text-black rounded-2xl font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center gap-4 shadow-xl"
>
<span>View Reconciler</span>
<ArrowUpRight size={18} />
</button>
</div>
</div>
{/* Provision Modal */}
{isProvisionOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 backdrop-blur-sm bg-black/60">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-lg rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95">
<div className="flex justify-between items-start mb-10">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-blue-600/10 text-blue-500 rounded-2xl flex items-center justify-center">
<Network size={28} />
</div>
<div>
<h3 className="text-2xl font-black text-white italic tracking-tighter uppercase">Provision <span className="text-blue-500 not-italic">Node</span></h3>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest">Initialize Virtual Data Sink</p>
</div>
</div>
<button onClick={() => setIsProvisionOpen(false)} className="p-2 text-zinc-600 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="space-y-6">
{provisionError && (
<div className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-2xl flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
<AlertCircle size={16} className="text-rose-500 shrink-0" />
<p className="text-[10px] font-bold text-rose-200 uppercase tracking-tight leading-relaxed">{provisionError}</p>
</div>
)}
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Node Name</label>
<input
value={newVa.name}
onChange={e => {
setProvisionError(null);
setNewVa({...newVa, name: e.target.value});
}}
placeholder="e.g. Q4 Marketing Buffer"
className="w-full bg-black border border-zinc-800 focus:border-blue-500/50 rounded-2xl py-4 px-5 text-white text-sm outline-none transition-all placeholder:text-zinc-800 font-bold"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Parent Account</label>
<select
value={newVa.parent}
onChange={e => {
setProvisionError(null);
setNewVa({...newVa, parent: e.target.value});
}}
className="w-full bg-black border border-zinc-800 focus:border-blue-500/50 rounded-2xl py-4 px-5 text-white text-sm outline-none transition-all font-bold appearance-none"
>
<option value="IA-4401">IA-4401 (J.P. Morgan)</option>
<option value="IA-9902">IA-9902 (Deutsche Bank)</option>
<option value="IA-1105">IA-1105 (Custody)</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Contextual Description</label>
<textarea
value={newVa.description}
onChange={e => {
setProvisionError(null);
setNewVa({...newVa, description: e.target.value});
}}
placeholder="Routing purpose and ledger mapping..."
className="w-full bg-black border border-zinc-800 focus:border-blue-500/50 rounded-2xl py-4 px-5 text-white text-sm outline-none transition-all placeholder:text-zinc-800 font-bold h-24 resize-none"
/>
</div>
<button
onClick={handleProvision}
disabled={provisioning || !newVa.name.trim()}
className="w-full py-5 bg-blue-600 hover:bg-blue-500 text-white rounded-[2rem] font-black text-xs uppercase tracking-[0.3em] transition-all flex items-center justify-center gap-4 shadow-xl shadow-blue-900/40 disabled:opacity-50"
>
{provisioning ? <Loader2 className="animate-spin" size={20} /> : <ArrowUpRight size={20} />}
<span>{provisioning ? 'Initializing Handshake...' : 'Establish Virtual Node'}</span>
</button>
</div>
</div>
</div>
)}
{/* Config Modal */}
{isConfigOpen && selectedVa && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 backdrop-blur-sm bg-black/60">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-lg rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95">
<div className="flex justify-between items-start mb-10">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-zinc-900 border border-zinc-800 rounded-2xl flex items-center justify-center text-blue-500">
<Settings2 size={28} />
</div>
<div>
<h3 className="text-2xl font-black text-white italic tracking-tighter uppercase">Node <span className="text-blue-500 not-italic">Config</span></h3>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest">{selectedVa.id}</p>
</div>
</div>
<button onClick={() => setIsConfigOpen(false)} className="p-2 text-zinc-600 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="space-y-8">
<div className="p-6 bg-black rounded-3xl border border-zinc-900 flex items-center justify-between">
<div>
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest mb-1">Signal Status</p>
<p className="text-white font-bold text-lg italic uppercase">{selectedVa.status}</p>
</div>
<div className="w-3 h-3 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_8px_#10b981]"></div>
</div>
<div className="grid grid-cols-2 gap-4">
<button className="p-5 bg-zinc-900 hover:bg-zinc-800 text-white rounded-2xl border border-zinc-800 transition-all font-black text-[10px] uppercase tracking-widest">
Rotate Keys
</button>
<button className="p-5 bg-zinc-900 hover:bg-zinc-800 text-white rounded-2xl border border-zinc-800 transition-all font-black text-[10px] uppercase tracking-widest">
Flush Cache
</button>
</div>
<div className="p-6 bg-blue-600/5 border border-blue-500/20 rounded-3xl">
<p className="text-[9px] font-black text-blue-500 uppercase tracking-widest mb-3 flex items-center gap-2">
<Activity size={10} />
Network Payload Metadata
</p>
<p className="text-[10px] text-zinc-500 font-mono leading-relaxed">
Created: {new Date(selectedVa.created_at).toLocaleString()}<br />
Routing: FDX_V6_NATIVE_BRIDGE<br />
Latency: 12ms avg.
</p>
</div>
<button
onClick={() => setIsConfigOpen(false)}
className="w-full py-4 bg-zinc-100 hover:bg-white text-black rounded-2xl font-black text-xs uppercase tracking-widest transition-all"
>
Close Configuration
</button>
</div>
</div>
</div>
)}
{/* Reconciler Modal */}
{isReconcilerOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 backdrop-blur-sm bg-black/60">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-lg rounded-[3rem] p-10 shadow-2xl animate-in slide-in-from-bottom-6">
<div className="flex justify-between items-start mb-10">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-emerald-500/10 text-emerald-500 rounded-2xl flex items-center justify-center">
<ShieldCheck size={28} />
</div>
<div>
<h3 className="text-2xl font-black text-white italic tracking-tighter uppercase">Quantum <span className="text-emerald-500 not-italic">Reconciler</span></h3>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest">Automated Signal Matching Engine</p>
</div>
</div>
<button onClick={() => setIsReconcilerOpen(false)} className="p-2 text-zinc-600 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="space-y-8">
<div className="bg-black border border-zinc-900 rounded-[2.5rem] overflow-hidden shadow-2xl h-48 flex flex-col">
<div className="bg-zinc-900/80 px-6 py-3 border-b border-zinc-800 flex justify-between items-center">
<div className="flex items-center gap-2">
<Terminal size={12} className="text-blue-500" />
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-500">Live Matching Console</span>
</div>
<div className="flex items-center gap-2">
<Activity size={12} className="text-emerald-500 animate-pulse" />
<span className="text-[8px] font-black uppercase text-zinc-600">Sync Active</span>
</div>
</div>
<div className="flex-1 p-6 font-mono text-[10px] overflow-y-auto custom-scrollbar space-y-2">
<div className="text-zinc-500">[14:22:01] Received signal VA-88021...</div>
<div className="text-zinc-500">[14:22:02] Matching against Treasury Operating Account (IA-4401)...</div>
<div className="text-emerald-500 font-bold">[14:22:03] MATCH FOUND: 100% confidence. Ledger updated.</div>
<div className="text-zinc-500">[14:22:10] Scanning global fabric for adrift payloads...</div>
</div>
</div>
<div className="p-8 bg-zinc-900/50 border border-zinc-800 rounded-[2rem] relative group">
<div className="absolute top-0 right-0 p-6 opacity-5">
<Cpu size={80} />
</div>
<div className="flex items-center gap-3 mb-4">
<CheckCircle2 size={18} className="text-emerald-500" />
<h4 className="text-[10px] font-black text-white uppercase tracking-widest">LQI Intelligence Report</h4>
</div>
{reconciling ? (
<div className="flex items-center gap-4 text-zinc-500">
<Loader2 size={20} className="animate-spin text-blue-500" />
<span className="text-[10px] font-black uppercase tracking-[0.4em] animate-pulse">Running Neural Pattern Analysis...</span>
</div>
) : (
<p className="text-sm font-medium text-zinc-300 italic leading-relaxed">
"{reconcileReport}"
</p>
)}
</div>
<div className="flex gap-4">
<button
onClick={handleRunReconciler}
disabled={reconciling}
className="flex-1 py-4 bg-zinc-900 hover:bg-zinc-800 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest border border-zinc-800 flex items-center justify-center gap-3"
>
<RefreshCw size={14} className={reconciling ? 'animate-spin' : ''} />
<span>{reconciling ? 'Re-analyzing...' : 'Refresh Audit'}</span>
</button>
<button
onClick={() => setIsReconcilerOpen(false)}
className="flex-1 py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest shadow-xl shadow-blue-900/20"
>
Confirm Audits
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default VirtualAccounts;