From ed7d2896dde5c1874531f3ac4df53276550da7bf Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 19 Jan 2026 09:15:06 +0300 Subject: [PATCH] Update AdminDashboard.tsx --- src/pages/AdminDashboard.tsx | 425 +++++++++++++++++++++++++++++++---- 1 file changed, 376 insertions(+), 49 deletions(-) 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 = () => ( @@ -72,6 +73,36 @@ const RestartIcon = () => ( ) +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' + })} + +
+
+ ))} +
+
+ )} ) }