Amba-Checkout/src/components/admin/TransactionList.tsx
2025-12-21 21:44:04 +03:00

362 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};