QuantumShield / frontend /components /TransactionTable.tsx
SantoshKumar1310's picture
Upload folder using huggingface_hub
49e53ae verified
'use client'
import { useState, useMemo } from 'react'
import { Search, ChevronDown, ChevronUp, AlertTriangle, CheckCircle } from 'lucide-react'
interface Transaction {
id: number
amount: number
merchant: string
category: string
is_fraud: number
prediction: string
final_score: number
classical_score: number
quantum_score: number
}
interface TransactionTableProps {
transactions: Transaction[]
compact?: boolean
}
export default function TransactionTable({ transactions, compact = false }: TransactionTableProps) {
const [search, setSearch] = useState('')
const [sortField, setSortField] = useState<keyof Transaction>('id')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc')
const [filterPrediction, setFilterPrediction] = useState<'all' | 'Fraud' | 'Safe'>('all')
const [page, setPage] = useState(0)
const pageSize = compact ? 10 : 20
const filteredTransactions = useMemo(() => {
let result = [...transactions]
// Filter by search
if (search) {
const searchLower = search.toLowerCase()
result = result.filter(t =>
t.merchant?.toLowerCase().includes(searchLower) ||
t.category?.toLowerCase().includes(searchLower) ||
t.id.toString().includes(searchLower)
)
}
// Filter by prediction
if (filterPrediction !== 'all') {
result = result.filter(t => t.prediction === filterPrediction)
}
// Sort
result.sort((a, b) => {
const aVal = a[sortField]
const bVal = b[sortField]
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal
}
const aStr = String(aVal).toLowerCase()
const bStr = String(bVal).toLowerCase()
return sortDirection === 'asc'
? aStr.localeCompare(bStr)
: bStr.localeCompare(aStr)
})
return result
}, [transactions, search, sortField, sortDirection, filterPrediction])
const paginatedTransactions = useMemo(() => {
const start = page * pageSize
return filteredTransactions.slice(start, start + pageSize)
}, [filteredTransactions, page, pageSize])
const totalPages = Math.ceil(filteredTransactions.length / pageSize)
const handleSort = (field: keyof Transaction) => {
if (sortField === field) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortDirection('desc')
}
}
const SortIcon = ({ field }: { field: keyof Transaction }) => {
if (sortField !== field) return null
return sortDirection === 'asc'
? <ChevronUp className="w-4 h-4" />
: <ChevronDown className="w-4 h-4" />
}
if (transactions.length === 0) {
return (
<div className="text-center py-8 text-dark-400">
<p>No transactions to display</p>
</div>
)
}
return (
<div className="space-y-4">
{/* Filters - only show if not compact */}
{!compact && (
<div className="flex flex-wrap gap-4">
{/* Search */}
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-dark-400" />
<input
type="text"
placeholder="Search merchant, category, ID..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-dark-700 border border-dark-600 rounded-lg pl-10 pr-4 py-2 text-sm focus:border-primary-500 focus:outline-none"
/>
</div>
{/* Filter dropdown */}
<select
value={filterPrediction}
onChange={(e) => setFilterPrediction(e.target.value as any)}
className="bg-dark-700 border border-dark-600 rounded-lg px-4 py-2 text-sm focus:border-primary-500 focus:outline-none"
>
<option value="all">All Predictions</option>
<option value="Fraud">Fraud Only</option>
<option value="Safe">Safe Only</option>
</select>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-700">
<th
className="text-left py-3 px-4 text-dark-300 font-medium cursor-pointer hover:text-white"
onClick={() => handleSort('id')}
>
<div className="flex items-center gap-1">
ID <SortIcon field="id" />
</div>
</th>
<th
className="text-left py-3 px-4 text-dark-300 font-medium cursor-pointer hover:text-white"
onClick={() => handleSort('amount')}
>
<div className="flex items-center gap-1">
Amount <SortIcon field="amount" />
</div>
</th>
{!compact && (
<>
<th className="text-left py-3 px-4 text-dark-300 font-medium">Merchant</th>
<th className="text-left py-3 px-4 text-dark-300 font-medium">Category</th>
</>
)}
<th
className="text-left py-3 px-4 text-dark-300 font-medium cursor-pointer hover:text-white"
onClick={() => handleSort('final_score')}
>
<div className="flex items-center gap-1">
Risk Score <SortIcon field="final_score" />
</div>
</th>
<th className="text-left py-3 px-4 text-dark-300 font-medium">Status</th>
<th className="text-left py-3 px-4 text-dark-300 font-medium">Actual</th>
</tr>
</thead>
<tbody>
{paginatedTransactions.map((t) => (
<tr
key={t.id}
className={`
border-b border-dark-800 hover:bg-dark-800/50 transition-colors
${t.prediction === 'Fraud' ? 'bg-red-500/5' : ''}
`}
>
<td className="py-3 px-4 font-mono text-dark-300">#{t.id}</td>
<td className="py-3 px-4 font-semibold">${t.amount.toFixed(2)}</td>
{!compact && (
<>
<td className="py-3 px-4 text-dark-300 truncate max-w-[150px]">{t.merchant}</td>
<td className="py-3 px-4 text-dark-400">{t.category}</td>
</>
)}
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-dark-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
t.final_score > 0.7 ? 'bg-red-500' :
t.final_score > 0.4 ? 'bg-yellow-500' : 'bg-green-500'
}`}
style={{ width: `${t.final_score * 100}%` }}
/>
</div>
<span className="text-xs text-dark-400">
{(t.final_score * 100).toFixed(0)}%
</span>
</div>
</td>
<td className="py-3 px-4">
{t.prediction === 'Fraud' ? (
<span className="badge-fraud flex items-center gap-1 w-fit">
<AlertTriangle className="w-3 h-3" /> Fraud
</span>
) : (
<span className="badge-safe flex items-center gap-1 w-fit">
<CheckCircle className="w-3 h-3" /> Safe
</span>
)}
</td>
<td className="py-3 px-4">
<span className={t.is_fraud ? 'text-red-400' : 'text-green-400'}>
{t.is_fraud ? '⚠️ Fraud' : '✓ Safe'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination - only show if not compact and multiple pages */}
{!compact && totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<p className="text-sm text-dark-400">
Showing {page * pageSize + 1} - {Math.min((page + 1) * pageSize, filteredTransactions.length)} of {filteredTransactions.length}
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0}
className="btn-secondary text-sm py-1 px-3 disabled:opacity-50"
>
Previous
</button>
<span className="text-sm text-dark-400">
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
disabled={page >= totalPages - 1}
className="btn-secondary text-sm py-1 px-3 disabled:opacity-50"
>
Next
</button>
</div>
</div>
)}
</div>
)
}