Spaces:
Sleeping
Sleeping
File size: 12,010 Bytes
b8b3edf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | import React, { useState, useEffect } from 'react';
import {
History,
Search,
Building2,
ShieldCheck,
Loader2,
ArrowDownLeft,
ArrowUpRight,
RefreshCw,
Calendar
} from 'lucide-react';
import { apiClient } from '../services/api';
import { CitiTransaction, InternalAccount } from '../types/index';
const Payments: React.FC = () => {
const [activeTab, setActiveTab] = useState<'registry' | 'history'>('registry');
const [loading, setLoading] = useState(true);
const [accounts, setAccounts] = useState<InternalAccount[]>([]);
const [selectedAccountId, setSelectedAccountId] = useState<string | null>(null);
const [transactions, setTransactions] = useState<CitiTransaction[]>([]);
const [loadingTx, setLoadingTx] = useState(false);
const fetchRegistry = async () => {
setLoading(true);
try {
const data = await apiClient.getRegistryDetails();
const mapped: InternalAccount[] = data.accountGroupDetails.flatMap(group => {
const list: InternalAccount[] = [];
if (group.checkingAccountsDetails) {
group.checkingAccountsDetails.forEach(acc => list.push({
id: acc.accountId,
productName: acc.productName,
displayAccountNumber: acc.displayAccountNumber,
currency: acc.currencyCode,
status: acc.accountStatus,
currentBalance: acc.currentBalance,
availableBalance: acc.availableBalance,
institutionName: 'Citi US',
connectionId: 'CITI-G-001'
}));
}
return list;
});
setAccounts(mapped);
} catch (e) {
console.error("Registry Node Sync Failure");
} finally {
setLoading(false);
}
};
const fetchTransactions = async (accountId: string) => {
setLoadingTx(true);
try {
const apiTx = await apiClient.getTransactions(accountId);
setTransactions(apiTx);
setSelectedAccountId(accountId);
setActiveTab('history');
} catch (e) {
console.error("Transaction Handshake Failed");
} finally {
setLoadingTx(false);
}
};
useEffect(() => {
fetchRegistry();
}, []);
return (
<div className="space-y-10 animate-in fade-in duration-700 pb-20">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div>
<h2 className="text-4xl font-black text-white italic tracking-tighter uppercase mb-2">Registry <span className="text-blue-500 not-italic">Settlement</span></h2>
<p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.3em] flex items-center gap-2">
<ShieldCheck size={12} className="text-emerald-500" />
Direct Node Handshake Active
</p>
</div>
<div className="flex gap-4">
<div className="flex bg-black p-1.5 rounded-2xl border border-zinc-900">
<button
onClick={() => setActiveTab('registry')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest ${activeTab === 'registry' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
Nodes
</button>
<button
onClick={() => setActiveTab('history')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest ${activeTab === 'history' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
Ledger
</button>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-8">
<div className="col-span-12 lg:col-span-4 space-y-6">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] p-8 shadow-2xl flex flex-col h-full relative overflow-hidden">
<div className="relative z-10 mb-8">
<h3 className="text-white font-black text-xs uppercase tracking-widest italic mb-2">Available Nodes</h3>
<p className="text-[9px] text-zinc-600 font-bold uppercase tracking-widest">Select target registry for granular audit</p>
</div>
<div className="space-y-4 flex-1 overflow-y-auto custom-scrollbar pr-2">
{loading ? (
Array.from({ length: 3 }).map((_, i) => <div key={i} className="h-24 bg-zinc-900 rounded-2xl animate-pulse"></div>)
) : (
accounts.map(acc => (
<button
key={acc.id}
onClick={() => fetchTransactions(acc.id)}
className={`w-full text-left p-6 rounded-[2rem] border transition-all group ${selectedAccountId === acc.id ? 'bg-blue-600/10 border-blue-500 shadow-xl' : 'bg-black border-zinc-900 hover:border-zinc-700'}`}
>
<div className="flex justify-between items-start mb-4">
<div className={`p-3 rounded-xl ${selectedAccountId === acc.id ? 'bg-blue-500 text-white' : 'bg-zinc-900 text-zinc-600 group-hover:text-blue-400'}`}>
<Building2 size={20} />
</div>
<span className={`text-[8px] font-black uppercase px-2 py-0.5 rounded border ${acc.status === 'ACTIVE' ? 'text-emerald-500 border-emerald-500/20' : 'text-zinc-500 border-zinc-800'}`}>{acc.status}</span>
</div>
<h4 className="text-white font-black text-xs uppercase italic truncate">{acc.productName}</h4>
<div className="mt-4 flex justify-between items-baseline">
<p className="text-[10px] text-zinc-600 font-bold mono tracking-tighter">{acc.displayAccountNumber}</p>
<p className="text-sm font-black text-white mono">${acc.currentBalance.toLocaleString()}</p>
</div>
</button>
))
)}
</div>
</div>
</div>
<div className="col-span-12 lg:col-span-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] overflow-hidden shadow-2xl min-h-[600px] flex flex-col">
<div className="p-8 border-b border-zinc-900 bg-black/20 flex justify-between items-center">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-blue-600/10 text-blue-500 rounded-2xl flex items-center justify-center">
<History size={24} />
</div>
<div>
<h3 className="text-white font-black uppercase tracking-[0.2em] italic text-sm">Ledger Audit Trace</h3>
<p className="text-[9px] text-zinc-600 font-bold uppercase tracking-widest mt-1">
{selectedAccountId ? `SYNCED_NODE: ${selectedAccountId}` : 'AWAITING_NODE_HANDSHAKE'}
</p>
</div>
</div>
<div className="relative">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-700" />
<input
placeholder="Filter handshakes..."
className="bg-black border border-zinc-800 rounded-xl py-2 pl-10 pr-4 text-[10px] font-black uppercase tracking-widest text-white outline-none focus:border-blue-500/50 w-48"
/>
</div>
</div>
<div className="flex-1 p-0 overflow-y-auto custom-scrollbar">
{loadingTx ? (
<div className="h-full flex flex-col items-center justify-center py-20 gap-4">
<Loader2 className="animate-spin text-blue-500" size={48} />
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-[0.4em] animate-pulse">Requesting Registry Payload...</p>
</div>
) : !selectedAccountId ? (
<div className="h-full flex flex-col items-center justify-center py-20 text-center space-y-4 opacity-30 grayscale">
<ShieldCheck size={80} className="text-zinc-800" />
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-[0.4em]">Node Connection Idle</p>
</div>
) : (
<table className="w-full text-left">
<thead>
<tr className="text-[9px] font-black uppercase tracking-[0.3em] text-zinc-600 border-b border-zinc-900 bg-black/40">
<th className="p-8">Handshake Detail</th>
<th className="p-8">Protocol/Type</th>
<th className="p-8">Status</th>
<th className="p-8 text-right">Value (USD)</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-900/50">
{transactions.map(tx => (
<tr key={tx.transactionId} className="group hover:bg-white/[0.01] transition-colors">
<td className="p-8">
<div className="flex items-center gap-6">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center border transition-all ${tx.transactionAmount < 0 ? 'bg-rose-500/10 border-rose-500/20 text-rose-500' : 'bg-emerald-500/10 border-emerald-500/20 text-emerald-500'}`}>
{tx.transactionAmount < 0 ? <ArrowUpRight size={18} /> : <ArrowDownLeft size={18} />}
</div>
<div>
<p className="text-xs font-black text-white uppercase italic leading-none">{tx.transactionDescription}</p>
<div className="flex items-center gap-3 mt-2">
<Calendar size={10} className="text-zinc-600" />
<p className="text-[9px] text-zinc-600 font-bold uppercase tracking-widest">{tx.transactionDate} • {tx.transactionId}</p>
</div>
</div>
</div>
</td>
<td className="p-8">
<span className="px-3 py-1 bg-zinc-900 border border-zinc-800 text-[9px] font-black text-zinc-500 uppercase tracking-widest rounded-lg">{tx.transactionType}</span>
</td>
<td className="p-8">
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full ${tx.transactionStatus === 'POSTED' || tx.transactionStatus === 'BILLED' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-amber-500'}`}></div>
<span className={`text-[10px] font-black uppercase tracking-widest ${tx.transactionStatus === 'POSTED' || tx.transactionStatus === 'BILLED' ? 'text-emerald-500' : 'text-amber-500'}`}>{tx.transactionStatus}</span>
</div>
</td>
<td className="p-8 text-right">
<p className={`text-sm font-black mono tracking-tighter ${tx.transactionAmount < 0 ? 'text-white' : 'text-emerald-500'}`}>
{tx.transactionAmount < 0 ? '' : '+'}${tx.transactionAmount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default Payments; |