diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx
index 4815ecc..909dcc3 100644
--- a/src/pages/AdminDashboard.tsx
+++ b/src/pages/AdminDashboard.tsx
@@ -1,7 +1,14 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
-import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin'
+import {
+ statsApi,
+ type DashboardStats,
+ type NodeStatus,
+ type TopReferrersResponse,
+ type TopCampaignsResponse,
+ type RecentPaymentsResponse
+} from '../api/admin'
import { useCurrency } from '../hooks/useCurrency'
// Icons - styled like main navigation
@@ -41,12 +48,6 @@ const SparklesIcon = () => (
)
-const CubeIcon = () => (
-
-)
-
const TagIcon = () => (
)
+const ChevronDownIcon = () => (
+
+)
+
+const UsersIcon = () => (
+
+)
+
+const MegaphoneIcon = () => (
+
+)
+
+const BanknotesIcon = () => (
+
+)
+
+const ExclamationIcon = () => (
+
+)
+
interface StatCardProps {
title: string
value: string | number
@@ -141,6 +172,8 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
return `${gb.toFixed(1)} GB`
}
+ const hasError = node.last_status_message && !node.is_connected
+
return (
@@ -156,6 +189,32 @@ function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) {
+ {/* Xray Version & Uptime */}
+ {(node.xray_version || node.xray_uptime) && (
+
+ {node.xray_version && (
+
+ Xray {node.xray_version}
+
+ )}
+ {node.xray_uptime && (
+
+ Uptime: {node.xray_uptime}
+
+ )}
+
+ )}
+
+ {/* Error Message */}
+ {hasError && (
+
+
+
+ {node.last_status_message}
+
+
+ )}
+
{t('adminDashboard.nodes.usersOnline')}
@@ -241,6 +300,13 @@ export default function AdminDashboard() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState
(null)
const [actionLoading, setActionLoading] = useState(null)
+ const [showAllNodes, setShowAllNodes] = useState(false)
+
+ // Extended stats
+ const [referrers, setReferrers] = useState(null)
+ const [campaigns, setCampaigns] = useState(null)
+ const [payments, setPayments] = useState(null)
+ const [referrersTab, setReferrersTab] = useState<'earnings' | 'invited'>('earnings')
const fetchStats = async () => {
try {
@@ -256,10 +322,29 @@ export default function AdminDashboard() {
}
}
+ const fetchExtendedStats = async () => {
+ try {
+ const [referrersData, campaignsData, paymentsData] = await Promise.all([
+ statsApi.getTopReferrers(10),
+ statsApi.getTopCampaigns(10),
+ statsApi.getRecentPayments(20),
+ ])
+ setReferrers(referrersData)
+ setCampaigns(campaignsData)
+ setPayments(paymentsData)
+ } catch (err) {
+ console.error('Failed to load extended stats:', err)
+ }
+ }
+
useEffect(() => {
fetchStats()
+ fetchExtendedStats()
// Refresh every 30 seconds
- const interval = setInterval(fetchStats, 30000)
+ const interval = setInterval(() => {
+ fetchStats()
+ fetchExtendedStats()
+ }, 30000)
return () => clearInterval(interval)
}, [])
@@ -393,17 +478,33 @@ export default function AdminDashboard() {
{stats?.nodes.nodes && stats.nodes.nodes.length > 0 ? (
-
- {stats.nodes.nodes.map((node) => (
-
- ))}
-
+ <>
+
+ {(showAllNodes ? stats.nodes.nodes : stats.nodes.nodes.slice(0, 3)).map((node) => (
+
+ ))}
+
+ {stats.nodes.nodes.length > 3 && (
+
+ )}
+ >
) : (
{t('adminDashboard.nodes.noNodes')}
@@ -499,36 +600,6 @@ export default function AdminDashboard() {
- {/* Server Stats */}
- {stats?.servers && (
-
-
-
-
-
-
{t('adminDashboard.servers.title')}
-
-
-
-
{t('adminDashboard.servers.total')}
-
{stats.servers.total_servers}
-
-
-
{t('adminDashboard.servers.available')}
-
{stats.servers.available_servers}
-
-
-
{t('adminDashboard.servers.withConnections')}
-
{stats.servers.servers_with_connections}
-
-
-
{t('adminDashboard.servers.revenue')}
-
{formatAmount(stats.servers.total_revenue_rubles)} {currencySymbol}
-
-
-
- )}
-
{/* Tariff Stats */}
{stats?.tariff_stats && stats.tariff_stats.tariffs.length > 0 && (
@@ -582,6 +653,262 @@ export default function AdminDashboard() {
)}
+
+ {/* Extended Stats Grid */}
+
+ {/* Top Referrers */}
+ {referrers && (referrers.by_earnings.length > 0 || referrers.by_invited.length > 0) && (
+
+
+
+
+
+
+
+
Топ рефералов
+
+ {referrers.total_referrers} реф., {referrers.total_referrals} пригл.
+
+
+
+
+
+ {/* Tabs */}
+
+
+
+
+
+
+ {(referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited).slice(0, 5).map((ref, idx) => (
+
+
+
+ {idx + 1}
+
+
+
{ref.display_name}
+ {ref.username &&
@{ref.username}
}
+
+
+
+ {referrersTab === 'earnings' ? (
+ <>
+
{formatAmount(ref.earnings_total_kopeks / 100)} {currencySymbol}
+
{ref.invited_count} пригл.
+ >
+ ) : (
+ <>
+
{ref.invited_count} чел.
+
{formatAmount(ref.earnings_total_kopeks / 100)} {currencySymbol}
+ >
+ )}
+
+
+ ))}
+
+
+ {/* Period Stats */}
+
+
+
Сегодня
+
+ {formatAmount(
+ (referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited)
+ .reduce((sum, r) => sum + r.earnings_today_kopeks, 0) / 100
+ )} {currencySymbol}
+
+
+
+
Неделя
+
+ {formatAmount(
+ (referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited)
+ .reduce((sum, r) => sum + r.earnings_week_kopeks, 0) / 100
+ )} {currencySymbol}
+
+
+
+
Месяц
+
+ {formatAmount(
+ (referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited)
+ .reduce((sum, r) => sum + r.earnings_month_kopeks, 0) / 100
+ )} {currencySymbol}
+
+
+
+
+ )}
+
+ {/* Top Campaigns */}
+ {campaigns && campaigns.campaigns.length > 0 && (
+
+
+
+
+
+
+
Топ РК ссылок
+
+ {campaigns.total_campaigns} камп., {campaigns.total_registrations} рег.
+
+
+
+
+
+ {campaigns.campaigns.slice(0, 5).map((campaign, idx) => (
+
+
+
+ {idx + 1}
+
+
+
{campaign.name}
+
?start={campaign.start_parameter}
+
+
+
+
{formatAmount(campaign.total_revenue_kopeks / 100)} {currencySymbol}
+
+ {campaign.registrations} · {campaign.conversion_rate.toFixed(0)}%
+
+
+
+ ))}
+
+
+
+
+ Всего от РК
+ {formatAmount(campaigns.total_revenue_kopeks / 100)} {currencySymbol}
+
+
+
+ )}
+
+
+ {/* Recent Payments */}
+ {payments && payments.payments.length > 0 && (
+
+
+
+
+
+
+
+
Последние платежи
+
+ Сегодня: {formatAmount(payments.total_today_kopeks / 100)} {currencySymbol}
+ · За неделю: {formatAmount(payments.total_week_kopeks / 100)} {currencySymbol}
+
+
+
+
+
+ {/* Desktop Table */}
+
+
+
+
+ | Пользователь |
+ Тип |
+ Сумма |
+ Метод |
+ Дата |
+
+
+
+ {payments.payments.slice(0, 10).map((payment) => (
+
+ |
+ {payment.display_name}
+ {payment.username && @{payment.username} }
+ |
+
+
+ {payment.type_display}
+
+ |
+
+ {formatAmount(payment.amount_rubles)} {currencySymbol}
+ |
+
+ {payment.payment_method || '-'}
+ |
+
+
+ {new Date(payment.created_at).toLocaleString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ |
+
+ ))}
+
+
+
+
+ {/* Mobile Cards */}
+
+ {payments.payments.slice(0, 10).map((payment) => (
+
+
+
+
+ {payment.type_display}
+
+ {payment.display_name}
+
+
+ {formatAmount(payment.amount_rubles)} {currencySymbol}
+
+
+
+ {payment.payment_method || '-'}
+
+ {new Date(payment.created_at).toLocaleString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+
+
+ ))}
+
+
+ )}
)
}