Spaces:
Build error
Build error
| '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> | |
| ) | |
| } | |