| 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); |
| const itemsPerPage = 15; |
|
|
| |
| const filteredPreview = (previewData || []).filter(item => { |
| |
| const searchStr = `${item.F3886 || ''} ${item.F3893 || ''} ${item.F3891 || ''} ${item.Unnamed_0 || item['Unnamed: 0'] || ''}`.toLowerCase(); |
| const matchesSearch = searchStr.includes(searchTerm.toLowerCase()); |
| |
| |
| const matchesRisk = riskFilter === 'All' || item.risk_level === riskFilter; |
| |
| return matchesSearch && matchesRisk; |
| }); |
|
|
| |
| 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> |
| ); |
| } |
|
|