aegis-fraud-detector / frontend /src /components /TransactionTable.jsx
WizardCoder2007's picture
Fresh deploy with clean history
31b612e
Raw
History Blame Contribute Delete
23.3 kB
import React, { useState } from 'react';
import {
FileText,
Search,
Download,
AlertTriangle,
ShieldAlert,
ShieldCheck,
UserX,
Eye
} from 'lucide-react';
export default function TransactionTable({ previewData, topSuspicious, muleAccounts, sessionId, backendUrl }) {
const [activeTab, setActiveTab] = useState('preview');
const [searchTerm, setSearchTerm] = useState('');
const [riskFilter, setRiskFilter] = useState('All');
const [currentPage, setCurrentPage] = useState(1);
const [selectedTx, setSelectedTx] = useState(null); // For detail modal
const itemsPerPage = 15;
// Filter & Search preview data
const filteredPreview = (previewData || []).filter(item => {
// Search matching
const searchStr = `${item.F3886 || ''} ${item.F3893 || ''} ${item.F3891 || ''} ${item.Unnamed_0 || item['Unnamed: 0'] || ''}`.toLowerCase();
const matchesSearch = searchStr.includes(searchTerm.toLowerCase());
// Risk matching
const matchesRisk = riskFilter === 'All' || item.risk_level === riskFilter;
return matchesSearch && matchesRisk;
});
// Pagination
const totalPages = Math.ceil(filteredPreview.length / itemsPerPage);
const paginatedData = filteredPreview.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
const getRiskBadge = (level) => {
switch (level) {
case 'High':
return <span className="badge badge-danger">High Risk</span>;
case 'Medium':
return <span className="badge badge-warning">Medium Risk</span>;
default:
return <span className="badge badge-success">Low Risk</span>;
}
};
const handleDownload = () => {
if (!sessionId) return;
window.open(`${backendUrl}/api/download-run/${sessionId}`, '_blank');
};
return (
<div className="glass-card animate-fade-in" style={{ marginTop: '24px' }}>
{/* Header and Download */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '16px',
marginBottom: '20px',
borderBottom: '1px solid var(--border-color)',
paddingBottom: '16px'
}}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<FileText style={{ color: 'var(--primary)' }} />
<h2 style={{ fontSize: '1.25rem', margin: 0 }}>Analysis Logs</h2>
</div>
{sessionId && (
<button className="btn btn-primary" onClick={handleDownload}>
<Download size={18} />
Download Annotated CSV
</button>
)}
</div>
{/* Tabs list */}
<div style={{
display: 'flex',
gap: '12px',
borderBottom: '1px solid var(--border-color)',
marginBottom: '20px',
overflowX: 'auto',
paddingBottom: '2px'
}}>
<button
style={{
background: 'none',
border: 'none',
color: activeTab === 'preview' ? 'var(--primary)' : 'var(--text-secondary)',
fontWeight: 600,
padding: '10px 16px',
borderBottom: activeTab === 'preview' ? '2px solid var(--primary)' : '2px solid transparent',
cursor: 'pointer',
transition: 'var(--transition)'
}}
onClick={() => { setActiveTab('preview'); setCurrentPage(1); }}
>
Transactions Preview ({filteredPreview.length})
</button>
<button
style={{
background: 'none',
border: 'none',
color: activeTab === 'alerts' ? 'var(--primary)' : 'var(--text-secondary)',
fontWeight: 600,
padding: '10px 16px',
borderBottom: activeTab === 'alerts' ? '2px solid var(--primary)' : '2px solid transparent',
cursor: 'pointer',
transition: 'var(--transition)'
}}
onClick={() => setActiveTab('alerts')}
>
Top Risky Alerts ({topSuspicious?.length || 0})
</button>
<button
style={{
background: 'none',
border: 'none',
color: activeTab === 'mules' ? 'var(--primary)' : 'var(--text-secondary)',
fontWeight: 600,
padding: '10px 16px',
borderBottom: activeTab === 'mules' ? '2px solid var(--primary)' : '2px solid transparent',
cursor: 'pointer',
transition: 'var(--transition)'
}}
onClick={() => setActiveTab('mules')}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<UserX size={16} />
Mule Account Candidates ({muleAccounts?.length || 0})
</div>
</button>
</div>
{/* SEARCH AND FILTERS (Only for preview tab) */}
{activeTab === 'preview' && (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '16px',
marginBottom: '20px'
}}>
{/* Search box */}
<div style={{ position: 'relative', width: '320px', maxWidth: '100%' }}>
<Search
size={18}
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--text-secondary)'
}}
/>
<input
type="text"
placeholder="Search by account, channel, job..."
className="form-input"
style={{ paddingLeft: '40px' }}
value={searchTerm}
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
/>
</div>
{/* Filter dropdown */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>Risk Filter:</span>
<select
className="form-input"
style={{ width: '150px', padding: '8px 12px' }}
value={riskFilter}
onChange={(e) => { setRiskFilter(e.target.value); setCurrentPage(1); }}
>
<option value="All">All Risk Levels</option>
<option value="High">High Risk</option>
<option value="Medium">Medium Risk</option>
<option value="Low">Low Risk</option>
</select>
</div>
</div>
)}
{/* TAB 1: ALL PREVIEW */}
{activeTab === 'preview' && (
<div>
{paginatedData.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
No transactions match the selected filters.
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
textAlign: 'left',
fontSize: '0.875rem'
}}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}>
<th style={{ padding: '12px' }}>ID</th>
<th style={{ padding: '12px' }}>Date</th>
<th style={{ padding: '12px' }}>Account Type</th>
<th style={{ padding: '12px' }}>Channel</th>
<th style={{ padding: '12px' }}>Job Type</th>
<th style={{ padding: '12px' }}>Risk Prob</th>
<th style={{ padding: '12px' }}>Status</th>
<th style={{ padding: '12px' }}>Actions</th>
</tr>
</thead>
<tbody>
{paginatedData.map((row, idx) => {
const rowId = row.Unnamed_0 || row['Unnamed: 0'] || `Row ${idx + 1}`;
return (
<tr
key={idx}
style={{
borderBottom: '1px solid var(--border-color)',
background: row.is_suspicious ? 'rgba(239, 68, 68, 0.03)' : 'transparent'
}}
>
<td style={{ padding: '12px', fontWeight: 600 }}>{rowId}</td>
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3888 || 'N/A'}</td>
<td style={{ padding: '12px' }}>{row.F3886 || 'N/A'}</td>
<td style={{ padding: '12px' }}>{row.F3893 || 'N/A'}</td>
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3891 || 'N/A'}</td>
<td style={{ padding: '12px', fontWeight: 700, color: row.is_suspicious ? 'var(--danger)' : 'var(--text-primary)' }}>
{Math.round(row.fraud_probability * 100)}%
</td>
<td style={{ padding: '12px' }}>
{getRiskBadge(row.risk_level)}
</td>
<td style={{ padding: '12px' }}>
<button
style={{
background: 'rgba(255,255,255,0.05)',
border: 'none',
borderRadius: '4px',
padding: '4px 8px',
cursor: 'pointer',
color: 'var(--text-secondary)'
}}
onClick={() => setSelectedTx(row)}
title="Inspect Details"
>
<Eye size={14} />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* Pagination controls */}
{totalPages > 1 && (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '20px',
borderTop: '1px solid var(--border-color)',
paddingTop: '16px'
}}>
<span style={{ fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className="btn btn-secondary"
style={{ padding: '6px 12px', fontSize: '0.8rem' }}
disabled={currentPage === 1}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
>
Previous
</button>
<button
className="btn btn-secondary"
style={{ padding: '6px 12px', fontSize: '0.8rem' }}
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
>
Next
</button>
</div>
</div>
)}
</div>
)}
{/* TAB 2: TOP SUSPICIOUS ALERTS */}
{activeTab === 'alerts' && (
<div style={{ overflowX: 'auto' }}>
{(!topSuspicious || topSuspicious.length === 0) ? (
<div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
No critical fraud alerts detected.
</div>
) : (
<table style={{
width: '100%',
borderCollapse: 'collapse',
textAlign: 'left',
fontSize: '0.875rem'
}}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}>
<th style={{ padding: '12px' }}>ID</th>
<th style={{ padding: '12px' }}>Risk Probability</th>
<th style={{ padding: '12px' }}>Date</th>
<th style={{ padding: '12px' }}>Account Type</th>
<th style={{ padding: '12px' }}>Channel</th>
<th style={{ padding: '12px' }}>Job Status</th>
<th style={{ padding: '12px' }}>Risk Assessment</th>
<th style={{ padding: '12px' }}>Action</th>
</tr>
</thead>
<tbody>
{topSuspicious.map((row, idx) => {
const rowId = row.Unnamed_0 || row['Unnamed: 0'] || `Alert ${idx + 1}`;
return (
<tr
key={idx}
style={{
borderBottom: '1px solid var(--border-color)',
background: 'rgba(239, 68, 68, 0.05)'
}}
>
<td style={{ padding: '12px', fontWeight: 600 }}>{rowId}</td>
<td style={{ padding: '12px', fontWeight: 800, color: 'var(--danger)', display: 'flex', alignItems: 'center', gap: '8px' }}>
<ShieldAlert size={16} />
{Math.round(row.fraud_probability * 100)}% Risk
</td>
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3888 || 'N/A'}</td>
<td style={{ padding: '12px' }}>{row.F3886 || 'N/A'}</td>
<td style={{ padding: '12px' }}>{row.F3893 || 'N/A'}</td>
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>{row.F3891 || 'N/A'}</td>
<td style={{ padding: '12px' }}>
<span className="badge badge-danger">High Threat</span>
</td>
<td style={{ padding: '12px' }}>
<button
className="btn btn-secondary"
style={{ padding: '4px 8px', fontSize: '0.75rem' }}
onClick={() => setSelectedTx(row)}
>
Inspect
</button>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
)}
{/* TAB 3: MULE ACCOUNT CANDIDATES */}
{activeTab === 'mules' && (
<div>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem', marginBottom: '16px' }}>
Mule accounts typically act as intermediaries to circulate stolen funds. The table below lists account IDs that have multiple high-risk/suspicious transactions in this file run.
</p>
<div style={{ overflowX: 'auto' }}>
{(!muleAccounts || muleAccounts.length === 0) ? (
<div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
No repeating account-level suspicious transactions detected.
</div>
) : (
<table style={{
width: '100%',
borderCollapse: 'collapse',
textAlign: 'left',
fontSize: '0.875rem'
}}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color)', color: 'var(--text-secondary)' }}>
<th style={{ padding: '12px' }}>Account ID / Value</th>
<th style={{ padding: '12px' }}>Total Transactions</th>
<th style={{ padding: '12px' }}>Suspicious Flagged Count</th>
<th style={{ padding: '12px' }}>Max Probability</th>
<th style={{ padding: '12px' }}>Avg Risk Probability</th>
<th style={{ padding: '12px' }}>Mule Account Threat Rating</th>
</tr>
</thead>
<tbody>
{muleAccounts.map((mule, idx) => {
const avgRisk = Math.round(mule.avg_prob * 100);
const threatScore = mule.suspicious_count * avgRisk;
const isSevere = threatScore > 100 || mule.suspicious_count >= 3;
return (
<tr
key={idx}
style={{
borderBottom: '1px solid var(--border-color)',
background: isSevere ? 'rgba(239, 68, 68, 0.04)' : 'transparent'
}}
>
<td style={{ padding: '12px', fontWeight: 600 }}>{mule.F3887 || mule.F3895 || `Acc #${idx}`}</td>
<td style={{ padding: '12px' }}>{mule.tx_count}</td>
<td style={{ padding: '12px', fontWeight: 700, color: 'var(--danger)' }}>{mule.suspicious_count}</td>
<td style={{ padding: '12px' }}>{Math.round(mule.max_prob * 100)}%</td>
<td style={{ padding: '12px', fontWeight: 600 }}>{avgRisk}%</td>
<td style={{ padding: '12px' }}>
{isSevere ? (
<span className="badge badge-danger" style={{ animation: 'pulse-glow 2s infinite' }}>
CRITICAL MULE
</span>
) : (
<span className="badge badge-warning">SUSPECT MULE</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
)}
{/* DETAILS MODAL */}
{selectedTx && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px'
}}>
<div className="glass-card animate-fade-in" style={{
width: '600px',
maxWidth: '100%',
maxHeight: '90vh',
overflowY: 'auto',
background: '#151e33',
border: '1px solid rgba(255,255,255,0.15)',
boxShadow: '0 10px 40px rgba(0,0,0,0.6)'
}}>
<h3 style={{ fontSize: '1.25rem', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<AlertTriangle style={{ color: selectedTx.is_suspicious ? 'var(--danger)' : 'var(--success)' }} />
Transaction Details & Analysis
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '20px', background: 'rgba(0,0,0,0.2)', padding: '16px', borderRadius: '8px' }}>
<div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Transaction ID</p>
<p style={{ fontWeight: 600 }}>{selectedTx.Unnamed_0 || selectedTx['Unnamed: 0'] || 'N/A'}</p>
</div>
<div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Risk Assessment</p>
<p>{getRiskBadge(selectedTx.risk_level)}</p>
</div>
<div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Fraud Probability</p>
<p style={{ fontWeight: 700, fontSize: '1.1rem', color: selectedTx.is_suspicious ? 'var(--danger)' : 'var(--success)' }}>
{Math.round(selectedTx.fraud_probability * 100)}%
</p>
</div>
<div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Transaction DateTime</p>
<p>{selectedTx.F3888 || 'N/A'}</p>
</div>
</div>
<h4 style={{ fontSize: '0.95rem', marginBottom: '8px', color: 'var(--text-secondary)' }}>Key Parameters Loaded</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', fontSize: '0.875rem', marginBottom: '24px' }}>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Account Type (F3886):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3886 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Channel (F3893):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3893 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Job Status (F3891):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3891 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Gender (F3892):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3892 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Account Code (F3887):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3887 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Customer Code (F3889):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3889 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Account State (F3890):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3890 || 'N/A'}</span>
</div>
<div style={{ borderBottom: '1px solid var(--border-color)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Tx Interval (F3894):</span> <span style={{ fontWeight: 500 }}>{selectedTx.F3894 || 'N/A'}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
<button className="btn btn-secondary" onClick={() => setSelectedTx(null)}>
Close Inspection
</button>
</div>
</div>
</div>
)}
</div>
);
}