362 lines
14 KiB
TypeScript
362 lines
14 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { api } from '../../utils/api';
|
||
import type { Transaction } from '../../types';
|
||
import { Card } from '../ui/Card';
|
||
import { Button } from '../ui/Button';
|
||
import { FiSearch, FiFilter, FiEye } from 'react-icons/fi';
|
||
|
||
export const TransactionList: React.FC = () => {
|
||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||
const [filteredTransactions, setFilteredTransactions] = useState<Transaction[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
|
||
|
||
useEffect(() => {
|
||
const fetchTransactions = async () => {
|
||
try {
|
||
const data = await api.getTransactions();
|
||
setTransactions(data);
|
||
setFilteredTransactions(data);
|
||
} catch (error) {
|
||
console.error('Failed to fetch transactions:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchTransactions();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
let filtered = transactions;
|
||
|
||
if (searchTerm) {
|
||
filtered = filtered.filter(
|
||
(t) =>
|
||
t.recipient.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
t.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
t.recipientEmail?.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
}
|
||
|
||
if (statusFilter !== 'all') {
|
||
filtered = filtered.filter((t) => t.status === statusFilter);
|
||
}
|
||
|
||
setFilteredTransactions(filtered);
|
||
}, [searchTerm, statusFilter, transactions]);
|
||
|
||
const formatCurrency = (amount: number) => {
|
||
return new Intl.NumberFormat('en-US', {
|
||
style: 'currency',
|
||
currency: 'USD',
|
||
}).format(amount);
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
};
|
||
|
||
const getStatusColor = (status: Transaction['status']) => {
|
||
switch (status) {
|
||
case 'approved':
|
||
return 'bg-green-100 text-green-800';
|
||
case 'rejected':
|
||
return 'bg-red-100 text-red-800';
|
||
case 'suspicious':
|
||
return 'bg-yellow-100 text-yellow-800';
|
||
default:
|
||
return 'bg-blue-100 text-blue-800';
|
||
}
|
||
};
|
||
|
||
const handleStatusUpdate = async (id: string, status: Transaction['status']) => {
|
||
try {
|
||
await api.updateTransactionStatus(id, status);
|
||
const updated = await api.getTransactions();
|
||
setTransactions(updated);
|
||
} catch (error) {
|
||
console.error('Failed to update transaction:', error);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<div className="text-gray-600">Loading...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<div className="mb-8">
|
||
<h1 className="text-3xl font-bold text-gray-900">Transactions</h1>
|
||
<p className="mt-2 text-gray-600">View and manage all transactions</p>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<Card className="p-4 mb-6">
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
<div className="flex-1 relative">
|
||
<FiSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search by recipient, ID, or email..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<FiFilter className="text-gray-400" />
|
||
<select
|
||
value={statusFilter}
|
||
onChange={(e) => setStatusFilter(e.target.value)}
|
||
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||
>
|
||
<option value="all">All Status</option>
|
||
<option value="pending">Pending</option>
|
||
<option value="approved">Approved</option>
|
||
<option value="rejected">Rejected</option>
|
||
<option value="suspicious">Suspicious</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Transaction Table */}
|
||
<Card className="overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="min-w-full divide-y divide-gray-200">
|
||
<thead className="bg-gray-50">
|
||
<tr>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
ID
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Recipient
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Amount
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Type
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Status
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Date
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Actions
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white divide-y divide-gray-200">
|
||
{filteredTransactions.map((transaction) => (
|
||
<tr key={transaction.id} className="hover:bg-gray-50">
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="text-sm font-mono text-gray-900">
|
||
{transaction.id.slice(0, 8)}...
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="text-sm font-medium text-gray-900">
|
||
{transaction.recipient}
|
||
</div>
|
||
{transaction.recipientEmail && (
|
||
<div className="text-sm text-gray-500">{transaction.recipientEmail}</div>
|
||
)}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<div className="text-sm font-medium text-gray-900">
|
||
{formatCurrency(transaction.amount)}
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<span className="text-sm text-gray-500 capitalize">
|
||
{transaction.accountType}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap">
|
||
<span
|
||
className={`px-2 py-1 text-xs font-medium rounded ${getStatusColor(
|
||
transaction.status
|
||
)}`}
|
||
>
|
||
{transaction.status}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||
{formatDate(transaction.createdAt)}
|
||
</td>
|
||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setSelectedTransaction(transaction)}
|
||
className="text-primary-600 hover:text-primary-900"
|
||
>
|
||
<FiEye className="w-5 h-5" />
|
||
</button>
|
||
{transaction.status === 'pending' && (
|
||
<>
|
||
<Button
|
||
size="sm"
|
||
variant="primary"
|
||
onClick={() => handleStatusUpdate(transaction.id, 'approved')}
|
||
>
|
||
Approve
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="danger"
|
||
onClick={() => handleStatusUpdate(transaction.id, 'rejected')}
|
||
>
|
||
Reject
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{filteredTransactions.length === 0 && (
|
||
<div className="text-center py-12">
|
||
<p className="text-gray-500">No transactions found</p>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
|
||
{/* Transaction Detail Modal */}
|
||
{selectedTransaction && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<Card className="max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="p-6">
|
||
<div className="flex justify-between items-start mb-6">
|
||
<h2 className="text-2xl font-bold text-gray-900">Transaction Details</h2>
|
||
<button
|
||
onClick={() => setSelectedTransaction(null)}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Transaction ID</p>
|
||
<p className="font-mono text-sm font-medium text-gray-900">
|
||
{selectedTransaction.id}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Amount</p>
|
||
<p className="text-lg font-bold text-gray-900">
|
||
{formatCurrency(selectedTransaction.amount)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Status</p>
|
||
<span
|
||
className={`inline-block px-3 py-1 text-sm font-medium rounded ${getStatusColor(
|
||
selectedTransaction.status
|
||
)}`}
|
||
>
|
||
{selectedTransaction.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-sm text-gray-600">Recipient</p>
|
||
<p className="font-medium text-gray-900">{selectedTransaction.recipient}</p>
|
||
{selectedTransaction.recipientEmail && (
|
||
<p className="text-sm text-gray-500">{selectedTransaction.recipientEmail}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-sm text-gray-600">Account Type</p>
|
||
<p className="font-medium text-gray-900 capitalize">
|
||
{selectedTransaction.accountType}
|
||
</p>
|
||
</div>
|
||
|
||
{selectedTransaction.description && (
|
||
<div>
|
||
<p className="text-sm text-gray-600">Description</p>
|
||
<p className="font-medium text-gray-900">
|
||
{selectedTransaction.description}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<p className="text-sm text-gray-600">Created At</p>
|
||
<p className="font-medium text-gray-900">
|
||
{formatDate(selectedTransaction.createdAt)}
|
||
</p>
|
||
</div>
|
||
|
||
{selectedTransaction.rejectionReason && (
|
||
<div>
|
||
<p className="text-sm text-gray-600">Rejection Reason</p>
|
||
<p className="font-medium text-red-600">
|
||
{selectedTransaction.rejectionReason}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-6 flex gap-4">
|
||
{selectedTransaction.status === 'pending' && (
|
||
<>
|
||
<Button
|
||
variant="primary"
|
||
onClick={() => {
|
||
handleStatusUpdate(selectedTransaction.id, 'approved');
|
||
setSelectedTransaction(null);
|
||
}}
|
||
>
|
||
Approve
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
onClick={() => {
|
||
handleStatusUpdate(selectedTransaction.id, 'rejected');
|
||
setSelectedTransaction(null);
|
||
}}
|
||
>
|
||
Reject
|
||
</Button>
|
||
</>
|
||
)}
|
||
<Button variant="outline" onClick={() => setSelectedTransaction(null)}>
|
||
Close
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|