278 lines
9.1 KiB
JavaScript
278 lines
9.1 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { RefreshCw, Download, Filter } from 'lucide-react';
|
|
import DataTable from '../components/DataTable';
|
|
import { paymentsAPI } from '../services/api';
|
|
|
|
const Transactions = () => {
|
|
const [payments, setPayments] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [filter, setFilter] = useState('all');
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchPayments();
|
|
}, []);
|
|
|
|
const fetchPayments = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError('');
|
|
const response = await paymentsAPI.getPayments();
|
|
setPayments(response.data || []);
|
|
} catch (err) {
|
|
setError('Failed to fetch transactions');
|
|
console.error('Transactions fetch error:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true);
|
|
await fetchPayments();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const handleFilter = (status) => {
|
|
setFilter(status);
|
|
};
|
|
|
|
const filteredPayments = payments.filter(payment => {
|
|
if (filter === 'all') return true;
|
|
return payment.status === filter;
|
|
});
|
|
|
|
const getStatusBadge = (status) => {
|
|
const statusClasses = {
|
|
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
|
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
|
pending: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
|
};
|
|
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClasses[status] || statusClasses.pending}`}>
|
|
{status}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const formatCurrency = (amount, currency = 'USD') => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency,
|
|
}).format(amount);
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const exportToCSV = () => {
|
|
const csvContent = [
|
|
['ID', 'User', 'Amount', 'Status', 'Date', 'Currency'],
|
|
...filteredPayments.map(payment => [
|
|
payment.id,
|
|
payment.user,
|
|
payment.amount,
|
|
payment.status,
|
|
formatDate(payment.date),
|
|
payment.currency
|
|
])
|
|
].map(row => row.join(',')).join('\n');
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `transactions-${new Date().toISOString().split('T')[0]}.csv`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
key: 'id',
|
|
header: 'Transaction ID',
|
|
render: (value) => (
|
|
<span className="font-mono text-sm font-medium text-gray-900 dark:text-white">
|
|
{value}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'user',
|
|
header: 'User',
|
|
render: (value) => (
|
|
<div className="flex items-center">
|
|
<div className="w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center mr-3">
|
|
<span className="text-xs font-medium text-primary-600 dark:text-primary-400">
|
|
{value.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<span className="text-sm font-medium text-gray-900 dark:text-white">{value}</span>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
key: 'amount',
|
|
header: 'Amount',
|
|
render: (value, row) => (
|
|
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{formatCurrency(value, row.currency)}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'status',
|
|
header: 'Status',
|
|
render: (value) => getStatusBadge(value)
|
|
},
|
|
{
|
|
key: 'date',
|
|
header: 'Date',
|
|
render: (value) => (
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
{formatDate(value)}
|
|
</span>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-8">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Transactions</h1>
|
|
<p className="text-gray-600 dark:text-gray-400">View and manage all payment transactions</p>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0 flex space-x-3">
|
|
<button
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
className="btn-secondary flex items-center"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
|
Refresh
|
|
</button>
|
|
<button
|
|
onClick={exportToCSV}
|
|
className="btn-primary flex items-center"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Export CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Filter className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Filter by status:</span>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
{[
|
|
{ key: 'all', label: 'All', count: payments.length },
|
|
{ key: 'success', label: 'Success', count: payments.filter(p => p.status === 'success').length },
|
|
{ key: 'pending', label: 'Pending', count: payments.filter(p => p.status === 'pending').length },
|
|
{ key: 'failed', label: 'Failed', count: payments.filter(p => p.status === 'failed').length },
|
|
].map(({ key, label, count }) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => handleFilter(key)}
|
|
className={`px-3 py-1 text-sm rounded-full transition-colors duration-200 ${
|
|
filter === key
|
|
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
{label} ({count})
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error State */}
|
|
{error && (
|
|
<div className="card p-6 text-center mb-6">
|
|
<p className="text-red-600 dark:text-red-400 mb-4">{error}</p>
|
|
<button onClick={fetchPayments} className="btn-primary">
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transactions Table */}
|
|
<DataTable
|
|
data={filteredPayments}
|
|
columns={columns}
|
|
loading={loading}
|
|
searchable={true}
|
|
className="shadow-sm"
|
|
/>
|
|
|
|
{/* Summary Stats */}
|
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Transactions</p>
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{filteredPayments.length}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-primary-100 dark:bg-primary-900 rounded-lg">
|
|
<div className="w-6 h-6 bg-primary-600 dark:bg-primary-400 rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Amount</p>
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{formatCurrency(
|
|
filteredPayments.reduce((sum, payment) => sum + payment.amount, 0)
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
|
|
<div className="w-6 h-6 bg-green-600 dark:bg-green-400 rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Success Rate</p>
|
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{filteredPayments.length > 0
|
|
? ((filteredPayments.filter(p => p.status === 'success').length / filteredPayments.length) * 100).toFixed(1)
|
|
: 0}%
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-400 rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Transactions;
|