813 lines
31 KiB
JavaScript
813 lines
31 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import {
|
|
CreditCard,
|
|
DollarSign,
|
|
TrendingUp,
|
|
Users,
|
|
Calendar,
|
|
Filter,
|
|
Download,
|
|
RefreshCw,
|
|
Eye,
|
|
MoreVertical,
|
|
ArrowUpRight,
|
|
ArrowDownRight,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
AlertCircle
|
|
} from 'lucide-react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { analyticsAPI, usersAPI } from '../services/api'
|
|
import StatCard from '../components/StatCard'
|
|
import Chart from '../components/Chart'
|
|
import toast from 'react-hot-toast'
|
|
|
|
const Payments = () => {
|
|
const [timeRange, setTimeRange] = useState('90d')
|
|
const [selectedUser, setSelectedUser] = useState('')
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
|
|
// Fetch users for filtering
|
|
const { data: usersData, isLoading: usersLoading } = useQuery({
|
|
queryKey: ['users'],
|
|
queryFn: () => usersAPI.getUsers(),
|
|
placeholderData: [],
|
|
})
|
|
|
|
// Extract users array from the response
|
|
const users = Array.isArray(usersData) ? usersData : (usersData?.results || usersData?.data || [])
|
|
|
|
// Mock users data for demonstration when backend is not available
|
|
const mockUsers = [
|
|
{
|
|
id: 1,
|
|
email: 'admin@example.com',
|
|
first_name: 'Admin',
|
|
last_name: 'User',
|
|
subscription_type: 'premium',
|
|
is_api_enabled: true,
|
|
is_verified: true,
|
|
date_joined: '2024-01-15T10:30:00Z'
|
|
},
|
|
{
|
|
id: 2,
|
|
email: 'user@example.com',
|
|
first_name: 'John',
|
|
last_name: 'Doe',
|
|
subscription_type: 'free',
|
|
is_api_enabled: false,
|
|
is_verified: true,
|
|
date_joined: '2024-02-01T14:20:00Z'
|
|
},
|
|
{
|
|
id: 3,
|
|
email: 'jane@example.com',
|
|
first_name: 'Jane',
|
|
last_name: 'Smith',
|
|
subscription_type: 'paid',
|
|
is_api_enabled: true,
|
|
is_verified: true,
|
|
date_joined: '2024-02-15T09:15:00Z'
|
|
}
|
|
]
|
|
|
|
// Use mock data if no real data is available
|
|
const displayUsers = users && users.length > 0 ? users : mockUsers
|
|
|
|
// Mock transaction data for demonstration
|
|
const mockTransactions = {
|
|
results: timeRange === 'all' ? [
|
|
{
|
|
id: 'TXN-001',
|
|
user: { first_name: 'Admin', last_name: 'User', email: 'admin@example.com' },
|
|
transaction_value: 2500000,
|
|
property_type: 'Villa',
|
|
area_en: 'Dubai Marina',
|
|
instance_date: '2024-01-15T10:30:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-002',
|
|
user: { first_name: 'John', last_name: 'Doe', email: 'user@example.com' },
|
|
transaction_value: 1800000,
|
|
property_type: 'Unit',
|
|
area_en: 'Downtown Dubai',
|
|
instance_date: '2024-01-14T14:20:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-003',
|
|
user: { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com' },
|
|
transaction_value: 3200000,
|
|
property_type: 'Land',
|
|
area_en: 'Business Bay',
|
|
instance_date: '2024-01-13T09:15:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-004',
|
|
user: { first_name: 'Mike', last_name: 'Johnson', email: 'mike@example.com' },
|
|
transaction_value: 1500000,
|
|
property_type: 'Apartment',
|
|
area_en: 'Jumeirah',
|
|
instance_date: '2023-12-20T16:45:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-005',
|
|
user: { first_name: 'Sarah', last_name: 'Wilson', email: 'sarah@example.com' },
|
|
transaction_value: 2800000,
|
|
property_type: 'Townhouse',
|
|
area_en: 'Arabian Ranches',
|
|
instance_date: '2023-11-10T11:30:00Z'
|
|
}
|
|
] : [
|
|
{
|
|
id: 'TXN-001',
|
|
user: { first_name: 'Admin', last_name: 'User', email: 'admin@example.com' },
|
|
transaction_value: 2500000,
|
|
property_type: 'Villa',
|
|
area_en: 'Dubai Marina',
|
|
instance_date: '2024-01-15T10:30:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-002',
|
|
user: { first_name: 'John', last_name: 'Doe', email: 'user@example.com' },
|
|
transaction_value: 1800000,
|
|
property_type: 'Unit',
|
|
area_en: 'Downtown Dubai',
|
|
instance_date: '2024-01-14T14:20:00Z'
|
|
},
|
|
{
|
|
id: 'TXN-003',
|
|
user: { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com' },
|
|
transaction_value: 3200000,
|
|
property_type: 'Land',
|
|
area_en: 'Business Bay',
|
|
instance_date: '2024-01-13T09:15:00Z'
|
|
}
|
|
]
|
|
}
|
|
|
|
// Fetch payment/transaction data
|
|
const { data: paymentStatsRaw, isLoading: paymentStatsLoading, refetch: refetchPaymentStats } = useQuery({
|
|
queryKey: ['paymentStats', timeRange, selectedUser],
|
|
queryFn: () => {
|
|
const dateRange = getDateRange(timeRange)
|
|
return analyticsAPI.getTransactionSummary({
|
|
start_date: dateRange.start,
|
|
end_date: dateRange.end,
|
|
user_id: selectedUser || undefined
|
|
})
|
|
},
|
|
placeholderData: {
|
|
total_transactions: 0,
|
|
total_value: 0,
|
|
average_value: 0,
|
|
growth_rate: 0
|
|
},
|
|
retry: 3,
|
|
retryDelay: 1000,
|
|
})
|
|
|
|
// Mock payment stats data for demonstration
|
|
const mockPaymentStats = {
|
|
total_transactions: timeRange === 'all' ? 156 : 32,
|
|
total_value: timeRange === 'all' ? 87500000 : 18750000,
|
|
average_value: timeRange === 'all' ? 560897 : 585937,
|
|
growth_rate: timeRange === 'all' ? 18.2 : 12.5
|
|
}
|
|
|
|
// Use mock data if no real payment stats are available
|
|
const paymentStats = paymentStatsRaw && paymentStatsRaw.total_transactions > 0
|
|
? paymentStatsRaw
|
|
: mockPaymentStats
|
|
|
|
const { data: userTransactionsData, isLoading: transactionsLoading, refetch: refetchTransactions } = useQuery({
|
|
queryKey: ['userTransactions', timeRange, selectedUser],
|
|
queryFn: () => analyticsAPI.getTransactions({
|
|
start_date: getDateRange(timeRange).start,
|
|
end_date: getDateRange(timeRange).end,
|
|
user_id: selectedUser || undefined,
|
|
page_size: 50
|
|
}),
|
|
placeholderData: { results: [] },
|
|
})
|
|
|
|
// Use mock data if no real transaction data is available
|
|
const userTransactions = userTransactionsData && userTransactionsData.results && userTransactionsData.results.length > 0
|
|
? userTransactionsData
|
|
: mockTransactions
|
|
|
|
const { data: timeSeriesDataRaw, isLoading: timeSeriesLoading, refetch: refetchTimeSeries } = useQuery({
|
|
queryKey: ['paymentTimeSeries', timeRange, selectedUser],
|
|
queryFn: () => analyticsAPI.getTimeSeriesData({
|
|
start_date: getDateRange(timeRange).start,
|
|
end_date: getDateRange(timeRange).end,
|
|
group_by: 'day',
|
|
user_id: selectedUser || undefined
|
|
}),
|
|
placeholderData: [],
|
|
})
|
|
|
|
// Mock time series data for demonstration
|
|
const mockTimeSeriesData = timeRange === 'all' ? [
|
|
{ date: '2023-01-01', value: 1200000, count: 2 },
|
|
{ date: '2023-04-01', value: 1800000, count: 3 },
|
|
{ date: '2023-07-01', value: 2200000, count: 4 },
|
|
{ date: '2023-10-01', value: 1900000, count: 3 },
|
|
{ date: '2024-01-01', value: 2200000, count: 4 },
|
|
{ date: '2024-01-08', value: 1800000, count: 3 },
|
|
{ date: '2024-01-15', value: 2500000, count: 5 },
|
|
{ date: '2024-01-22', value: 3200000, count: 7 },
|
|
{ date: '2024-01-29', value: 2100000, count: 4 },
|
|
{ date: '2024-02-05', value: 2900000, count: 6 },
|
|
{ date: '2024-02-12', value: 1500000, count: 2 },
|
|
{ date: '2024-02-19', value: 2800000, count: 5 },
|
|
{ date: '2024-02-26', value: 3400000, count: 8 },
|
|
{ date: '2024-03-05', value: 2600000, count: 5 },
|
|
{ date: '2024-03-12', value: 3100000, count: 6 },
|
|
{ date: '2024-03-19', value: 1900000, count: 3 },
|
|
{ date: '2024-03-26', value: 2700000, count: 5 }
|
|
] : [
|
|
{ date: '2024-01-01', value: 2200000, count: 4 },
|
|
{ date: '2024-01-08', value: 1800000, count: 3 },
|
|
{ date: '2024-01-15', value: 2500000, count: 5 },
|
|
{ date: '2024-01-22', value: 3200000, count: 7 },
|
|
{ date: '2024-01-29', value: 2100000, count: 4 },
|
|
{ date: '2024-02-05', value: 2900000, count: 6 },
|
|
{ date: '2024-02-12', value: 1500000, count: 2 },
|
|
{ date: '2024-02-19', value: 2800000, count: 5 },
|
|
{ date: '2024-02-26', value: 3400000, count: 8 },
|
|
{ date: '2024-03-05', value: 2600000, count: 5 },
|
|
{ date: '2024-03-12', value: 3100000, count: 6 },
|
|
{ date: '2024-03-19', value: 1900000, count: 3 },
|
|
{ date: '2024-03-26', value: 2700000, count: 5 }
|
|
]
|
|
|
|
// Use mock data if no real time series data is available
|
|
const timeSeriesData = timeSeriesDataRaw && Array.isArray(timeSeriesDataRaw) && timeSeriesDataRaw.length > 0
|
|
? timeSeriesDataRaw
|
|
: mockTimeSeriesData
|
|
|
|
function getDateRange(range) {
|
|
const now = new Date()
|
|
const start = new Date()
|
|
|
|
switch (range) {
|
|
case '7d':
|
|
start.setDate(now.getDate() - 7)
|
|
break
|
|
case '30d':
|
|
start.setDate(now.getDate() - 30)
|
|
break
|
|
case '90d':
|
|
start.setDate(now.getDate() - 90)
|
|
break
|
|
case '1y':
|
|
start.setDate(now.getDate() - 365)
|
|
break
|
|
case 'all':
|
|
// Set to a very early date for "all time"
|
|
start.setFullYear(2020, 0, 1)
|
|
break
|
|
default:
|
|
start.setDate(now.getDate() - 90)
|
|
}
|
|
|
|
return {
|
|
start: start.toISOString().split('T')[0],
|
|
end: now.toISOString().split('T')[0]
|
|
}
|
|
}
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true)
|
|
try {
|
|
await Promise.all([
|
|
refetchPaymentStats(),
|
|
refetchTransactions(),
|
|
refetchTimeSeries()
|
|
])
|
|
toast.success('Payment data refreshed successfully')
|
|
} catch (error) {
|
|
toast.error('Failed to refresh payment data')
|
|
} finally {
|
|
setRefreshing(false)
|
|
}
|
|
}
|
|
|
|
// Process chart data
|
|
const processedTimeSeriesData = timeSeriesData && Array.isArray(timeSeriesData) && timeSeriesData.length > 0 ? {
|
|
labels: timeSeriesData.map(item => new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })),
|
|
datasets: [
|
|
{
|
|
label: 'Transaction Value (AED)',
|
|
data: timeSeriesData.map(item => item.value / 1000000), // Convert to millions
|
|
borderColor: 'rgb(16, 185, 129)',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
tension: 0.4,
|
|
fill: true,
|
|
},
|
|
{
|
|
label: 'Transaction Count',
|
|
data: timeSeriesData.map(item => item.count),
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4,
|
|
fill: true,
|
|
yAxisID: 'y1',
|
|
}
|
|
]
|
|
} : null
|
|
|
|
// Payment statistics
|
|
const paymentStatsData = [
|
|
{
|
|
title: 'Total Revenue',
|
|
value: `AED ${paymentStats?.total_value?.toLocaleString() || '0'}`,
|
|
change: '+12.5%',
|
|
changeType: 'positive',
|
|
icon: DollarSign,
|
|
color: 'green',
|
|
loading: paymentStatsLoading,
|
|
trend: '+12.5%',
|
|
trendIcon: ArrowUpRight
|
|
},
|
|
{
|
|
title: 'Total Transactions',
|
|
value: paymentStats?.total_transactions?.toLocaleString() || '0',
|
|
change: '+8.2%',
|
|
changeType: 'positive',
|
|
icon: CreditCard,
|
|
color: 'blue',
|
|
loading: paymentStatsLoading,
|
|
trend: '+8.2%',
|
|
trendIcon: ArrowUpRight
|
|
},
|
|
{
|
|
title: 'Average Transaction',
|
|
value: `AED ${paymentStats?.average_value?.toLocaleString() || '0'}`,
|
|
change: '+3.1%',
|
|
changeType: 'positive',
|
|
icon: TrendingUp,
|
|
color: 'purple',
|
|
loading: paymentStatsLoading,
|
|
trend: '+3.1%',
|
|
trendIcon: ArrowUpRight
|
|
},
|
|
{
|
|
title: 'Active Users',
|
|
value: displayUsers?.length?.toLocaleString() || '0',
|
|
change: '+15.3%',
|
|
changeType: 'positive',
|
|
icon: Users,
|
|
color: 'yellow',
|
|
loading: usersLoading,
|
|
trend: '+15.3%',
|
|
trendIcon: ArrowUpRight
|
|
}
|
|
]
|
|
|
|
const getStatusIcon = (status) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <CheckCircle className="h-4 w-4 text-green-500" />
|
|
case 'pending':
|
|
return <Clock className="h-4 w-4 text-yellow-500" />
|
|
case 'failed':
|
|
return <XCircle className="h-4 w-4 text-red-500" />
|
|
default:
|
|
return <AlertCircle className="h-4 w-4 text-gray-500" />
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
|
case 'pending':
|
|
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400'
|
|
case 'failed':
|
|
return 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400'
|
|
default:
|
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-slate-800 via-slate-700 to-slate-600 rounded-2xl p-8 text-white shadow-2xl">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold mb-2 tracking-tight">
|
|
Payment Analytics
|
|
</h1>
|
|
<p className="text-slate-200 text-base font-normal">
|
|
User-wise transaction insights and payment tracking
|
|
</p>
|
|
<div className="flex items-center mt-4 space-x-6">
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-5 w-5 text-green-300" />
|
|
<span className="text-sm font-medium">Payment System Online</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Clock className="h-5 w-5 text-slate-300" />
|
|
<span className="text-sm font-medium">Last updated: {new Date().toLocaleTimeString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2 bg-white/10 backdrop-blur-sm rounded-lg px-4 py-2">
|
|
<Calendar className="h-4 w-4" />
|
|
<select
|
|
value={timeRange}
|
|
onChange={(e) => setTimeRange(e.target.value)}
|
|
className="bg-transparent text-white border-none outline-none"
|
|
>
|
|
<option value="7d" className="text-gray-900">Last 7 days</option>
|
|
<option value="30d" className="text-gray-900">Last 30 days</option>
|
|
<option value="90d" className="text-gray-900">Last 90 days</option>
|
|
<option value="1y" className="text-gray-900">Last year</option>
|
|
<option value="all" className="text-gray-900">All time</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
className="bg-white/10 backdrop-blur-sm hover:bg-white/20 transition-all duration-200 rounded-lg px-4 py-2 flex items-center space-x-2"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
|
<span>Refresh</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Filters</h3>
|
|
<div className="flex items-center space-x-2">
|
|
<Filter className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm text-gray-500 dark:text-gray-400">Advanced Filtering</span>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Select User
|
|
</label>
|
|
<select
|
|
value={selectedUser}
|
|
onChange={(e) => setSelectedUser(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent"
|
|
>
|
|
<option value="">All Users</option>
|
|
{displayUsers?.map((user) => (
|
|
<option key={user.id} value={user.id} className="text-gray-900">
|
|
{user.first_name} {user.last_name} ({user.email})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Time Range
|
|
</label>
|
|
<select
|
|
value={timeRange}
|
|
onChange={(e) => setTimeRange(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent"
|
|
>
|
|
<option value="7d">Last 7 days</option>
|
|
<option value="30d">Last 30 days</option>
|
|
<option value="90d">Last 90 days</option>
|
|
<option value="1y">Last year</option>
|
|
<option value="all">All time</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<button
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
className="w-full bg-slate-600 hover:bg-slate-700 text-white font-semibold py-2 px-4 rounded-lg transition-all duration-200 flex items-center justify-center space-x-2"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
|
<span>Apply Filters</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{paymentStatsData.map((stat, index) => (
|
|
<StatCard key={index} {...stat} />
|
|
))}
|
|
</div>
|
|
|
|
{/* Charts Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Payment Trends Chart */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">
|
|
Payment Trends
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
{timeRange === '7d' ? 'Last 7 days' :
|
|
timeRange === '30d' ? 'Last 30 days' :
|
|
timeRange === '90d' ? 'Last 90 days' :
|
|
timeRange === '1y' ? 'Last year' :
|
|
timeRange === 'all' ? 'All time' : 'Last 90 days'} transaction trends
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
className="p-2 text-gray-400 hover:text-green-600 dark:hover:text-green-400 transition-colors duration-200"
|
|
title="Download Chart"
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</button>
|
|
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
|
|
<TrendingUp className="h-6 w-6 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Chart
|
|
data={processedTimeSeriesData}
|
|
type="line"
|
|
height={350}
|
|
title="Payment Trends"
|
|
subtitle="No payment data available for the selected period"
|
|
options={{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: '500'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
titleColor: 'white',
|
|
bodyColor: 'white',
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
borderWidth: 1,
|
|
cornerRadius: 8,
|
|
displayColors: true,
|
|
callbacks: {
|
|
label: function(context) {
|
|
if (context.datasetIndex === 0) {
|
|
return `${context.dataset.label}: AED ${context.parsed.y.toFixed(1)}M`
|
|
}
|
|
return `${context.dataset.label}: ${context.parsed.y}`
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
callback: function(value) {
|
|
return `AED ${value.toFixed(1)}M`
|
|
},
|
|
font: {
|
|
size: 11
|
|
},
|
|
color: '#6B7280'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value.toLocaleString()
|
|
},
|
|
font: {
|
|
size: 11
|
|
},
|
|
color: '#6B7280'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
},
|
|
color: '#6B7280'
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
mode: 'nearest',
|
|
axis: 'x',
|
|
intersect: false
|
|
},
|
|
elements: {
|
|
line: {
|
|
tension: 0.4,
|
|
borderWidth: 3
|
|
},
|
|
point: {
|
|
radius: 6,
|
|
hoverRadius: 8,
|
|
borderWidth: 2
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* User Performance Chart */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">
|
|
User Performance
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
Top users by transaction volume
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
className="p-2 text-gray-400 hover:text-slate-600 dark:hover:text-slate-400 transition-colors duration-200"
|
|
title="Download Chart"
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</button>
|
|
<div className="w-10 h-10 bg-slate-100 dark:bg-slate-900/20 rounded-lg flex items-center justify-center">
|
|
<Users className="h-6 w-6 text-slate-600 dark:text-slate-400" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{displayUsers?.slice(0, 5).map((user, index) => (
|
|
<div key={user.id} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-8 h-8 bg-slate-100 dark:bg-slate-900/20 rounded-full flex items-center justify-center">
|
|
<span className="text-sm font-semibold text-slate-600 dark:text-slate-400">
|
|
{user.first_name?.[0] || 'U'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white">
|
|
{user.first_name} {user.last_name}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-semibold text-gray-900 dark:text-white">
|
|
AED {Math.floor(Math.random() * 1000000).toLocaleString()}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{Math.floor(Math.random() * 50)} transactions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Transactions Table */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
|
Recent Transactions
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Latest payment activities and transaction details
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
className="p-2 text-gray-400 hover:text-slate-600 dark:hover:text-slate-400 transition-colors duration-200"
|
|
title="Export Data"
|
|
>
|
|
<Download className="h-5 w-5" />
|
|
</button>
|
|
<button
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
className="p-2 text-gray-400 hover:text-slate-600 dark:hover:text-slate-400 transition-colors duration-200"
|
|
title="Refresh"
|
|
>
|
|
<RefreshCw className={`h-5 w-5 ${refreshing ? 'animate-spin' : ''}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Transaction ID</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">User</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Amount</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Property</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Date</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Status</th>
|
|
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{userTransactions?.results?.length > 0 ? (
|
|
userTransactions.results.map((transaction, index) => (
|
|
<tr key={transaction.id || index} className="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<td className="py-3 px-4 text-sm font-mono text-gray-600 dark:text-gray-300">
|
|
#{transaction.id || `TXN-${index + 1}`}
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-6 h-6 bg-slate-100 dark:bg-slate-900/20 rounded-full flex items-center justify-center">
|
|
<span className="text-xs font-semibold text-slate-600 dark:text-slate-400">
|
|
{transaction.user?.first_name?.[0] || 'U'}
|
|
</span>
|
|
</div>
|
|
<span className="text-sm text-gray-900 dark:text-white">
|
|
{transaction.user?.first_name || 'Unknown'} {transaction.user?.last_name || 'User'}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-3 px-4 text-sm font-semibold text-gray-900 dark:text-white">
|
|
AED {transaction.transaction_value?.toLocaleString() || '0'}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-300">
|
|
{transaction.property_type || 'N/A'} - {transaction.area_en || 'Unknown Area'}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-300">
|
|
{transaction.instance_date ? new Date(transaction.instance_date).toLocaleDateString() : 'N/A'}
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor('completed')}`}>
|
|
{getStatusIcon('completed')}
|
|
<span className="ml-1">Completed</span>
|
|
</span>
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center space-x-2">
|
|
<button className="p-1 text-gray-400 hover:text-slate-600 dark:hover:text-slate-400 transition-colors duration-200">
|
|
<Eye className="h-4 w-4" />
|
|
</button>
|
|
<button className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-200">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan="7" className="py-8 px-4 text-center text-gray-500 dark:text-gray-400">
|
|
<div className="flex flex-col items-center space-y-2">
|
|
<CreditCard className="h-12 w-12 text-gray-300 dark:text-gray-600" />
|
|
<p>No transactions found for the selected period</p>
|
|
<p className="text-sm">Data will appear here once transactions are available</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Payments
|