import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { statsApi, type DashboardStats, type NodeStatus, type TopReferrersResponse, type TopCampaignsResponse, type RecentPaymentsResponse, } from '../api/admin'; import { useCurrency } from '../hooks/useCurrency'; import { AdminBackButton } from '../components/admin'; // Icons - styled like main navigation const ServerIcon = () => ( ); const UsersOnlineIcon = () => ( ); const WalletIcon = () => ( ); const ChartBarIcon = () => ( ); const SparklesIcon = () => ( ); const TagIcon = () => ( ); const RefreshIcon = () => ( ); const PowerIcon = () => ( ); const RestartIcon = () => ( ); const ChevronDownIcon = () => ( ); const UsersIcon = () => ( ); const MegaphoneIcon = () => ( ); const BanknotesIcon = () => ( ); const ExclamationIcon = () => ( ); interface StatCardProps { title: string; value: string | number; subtitle?: string; icon: React.ReactNode; color: 'accent' | 'success' | 'warning' | 'error' | 'info'; trend?: { value: number; label: string; }; } function StatCard({ title, value, subtitle, icon, color, trend }: StatCardProps) { const colorClasses = { accent: 'bg-accent-500/20 text-accent-400', success: 'bg-success-500/20 text-success-400', warning: 'bg-warning-500/20 text-warning-400', error: 'bg-error-500/20 text-error-400', info: 'bg-info-500/20 text-info-400', }; return (
{icon}
{trend && (
= 0 ? 'bg-success-500/20 text-success-400' : 'bg-error-500/20 text-error-400'}`} > {trend.value >= 0 ? '+' : ''} {trend.value}% {trend.label}
)}
{value}
{title}
{subtitle &&
{subtitle}
}
); } interface NodeCardProps { node: NodeStatus; onRestart: (uuid: string) => void; onToggle: (uuid: string) => void; isLoading: boolean; } function NodeCard({ node, onRestart, onToggle, isLoading }: NodeCardProps) { const { t } = useTranslation(); const getStatusColor = () => { if (node.is_disabled) return 'bg-dark-600 text-dark-400'; if (node.is_connected) return 'bg-success-500/20 text-success-400'; return 'bg-error-500/20 text-error-400'; }; const getStatusText = () => { if (node.is_disabled) return t('adminDashboard.nodes.disabled'); if (node.is_connected) return t('adminDashboard.nodes.online'); return t('adminDashboard.nodes.offline'); }; const formatTraffic = (bytes?: number) => { if (!bytes) return '-'; const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1000) return `${(gb / 1000).toFixed(1)} TB`; return `${gb.toFixed(1)} GB`; }; const hasError = node.last_status_message && !node.is_connected; return (
{node.name}
{node.address}
{getStatusText()}
{/* 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')}
{node.users_online}
{t('adminDashboard.nodes.traffic')}
{formatTraffic(node.traffic_used_bytes)}
); } function RevenueChart({ data }: { data: { date: string; amount_rubles: number }[] }) { const { t } = useTranslation(); const { formatAmount, currencySymbol } = useCurrency(); if (!data || data.length === 0) { return (
{t('common.noData')}
); } const last7Days = data.slice(-7); const maxValue = Math.max(...last7Days.map((d) => d.amount_rubles), 1); return (
{last7Days.map((item) => { const percentage = (item.amount_rubles / maxValue) * 100; const date = new Date(item.date); const dayName = date.toLocaleDateString('ru-RU', { weekday: 'short' }); const dayNum = date.getDate(); return (
{dayName}, {dayNum} {formatAmount(item.amount_rubles)} {currencySymbol}
); })}
); } export default function AdminDashboard() { const { t } = useTranslation(); const { formatAmount, currencySymbol } = useCurrency(); const [stats, setStats] = useState(null); 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 { setLoading(true); setError(null); const data = await statsApi.getDashboardStats(); setStats(data); } catch (err) { setError(t('adminDashboard.loadError')); console.error('Failed to load dashboard stats:', err); } finally { setLoading(false); } }; 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(); fetchExtendedStats(); }, 30000); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleRestartNode = async (uuid: string) => { try { setActionLoading(uuid); await statsApi.restartNode(uuid); // Refresh stats after action setTimeout(fetchStats, 2000); } catch (err) { console.error('Failed to restart node:', err); } finally { setActionLoading(null); } }; const handleToggleNode = async (uuid: string) => { try { setActionLoading(uuid); await statsApi.toggleNode(uuid); await fetchStats(); } catch (err) { console.error('Failed to toggle node:', err); } finally { setActionLoading(null); } }; if (loading && !stats) { return (
); } if (error && !stats) { return (
{error}
); } return (
{/* Header */}

{t('adminDashboard.title')}

{t('adminDashboard.subtitle')}

{/* Main Stats Grid */}
} color="success" /> } color="accent" /> } color="warning" /> } color="info" />
{/* Nodes Section */}

{t('adminDashboard.nodes.title')}

{stats?.nodes.online || 0} {t('adminDashboard.nodes.online').toLowerCase()} /{' '} {stats?.nodes.total || 0} {t('adminDashboard.stats.total').toLowerCase()}

{stats?.nodes.online || 0} {stats?.nodes.offline || 0} {stats?.nodes.disabled || 0}
{stats?.nodes.nodes && stats.nodes.nodes.length > 0 ? ( <>
{(showAllNodes ? stats.nodes.nodes : stats.nodes.nodes.slice(0, 3)).map((node) => ( ))}
{stats.nodes.nodes.length > 3 && ( )} ) : (
{t('adminDashboard.nodes.noNodes')}
)}
{/* Revenue and Subscriptions */}
{/* Revenue Chart */}

{t('adminDashboard.revenue.title')}

{t('adminDashboard.revenue.last7Days')}

{t('adminDashboard.stats.incomeTotal')}
{formatAmount(stats?.financial.income_total_rubles || 0)} {currencySymbol}
{t('adminDashboard.stats.subscriptionIncome')}
{formatAmount(stats?.financial.subscription_income_rubles || 0)} {currencySymbol}
{/* Subscription Stats */}

{t('adminDashboard.subscriptions.title')}

{t('adminDashboard.subscriptions.subtitle')}

{t('adminDashboard.subscriptions.active')}
{stats?.subscriptions.active || 0}
{t('adminDashboard.subscriptions.trial')}
{stats?.subscriptions.trial || 0}
{t('adminDashboard.subscriptions.paid')}
{stats?.subscriptions.paid || 0}
{t('adminDashboard.subscriptions.expired')}
{stats?.subscriptions.expired || 0}
{t('adminDashboard.subscriptions.newSubscriptions')}
{stats?.subscriptions.purchased_today || 0}
{t('adminDashboard.subscriptions.today')}
{stats?.subscriptions.purchased_week || 0}
{t('adminDashboard.subscriptions.week')}
{stats?.subscriptions.purchased_month || 0}
{t('adminDashboard.subscriptions.month')}
{stats?.subscriptions.trial_to_paid_conversion !== undefined && (
{t('adminDashboard.subscriptions.conversion')} {stats.subscriptions.trial_to_paid_conversion.toFixed(1)}%
)}
{/* Tariff Stats */} {stats?.tariff_stats && stats.tariff_stats.tariffs.length > 0 && (

{t('adminDashboard.tariffs.title')}

{t('adminDashboard.tariffs.subtitle')}

{stats.tariff_stats.tariffs.map((tariff) => ( ))}
{t('adminDashboard.tariffs.tariffName')} {t('adminDashboard.tariffs.activeSubscriptions')} {t('adminDashboard.tariffs.trialSubscriptions')} {t('adminDashboard.tariffs.purchasedToday')} {t('adminDashboard.tariffs.purchasedWeek')} {t('adminDashboard.tariffs.purchasedMonth')}
{tariff.tariff_name} {tariff.active_subscriptions} {tariff.trial_subscriptions} {tariff.purchased_today} {tariff.purchased_week} {tariff.purchased_month}
)} {/* Extended Stats Grid */}
{/* Top Referrers */} {referrers && (referrers.by_earnings.length > 0 || referrers.by_invited.length > 0) && (

{t('adminDashboard.topReferrers.title')}

{referrers.total_referrers}{' '} {t('adminDashboard.topReferrers.stats', { count: 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} {t('adminDashboard.topReferrers.invites')}
) : ( <>
{ref.invited_count} {t('adminDashboard.topReferrers.people')}
{formatAmount(ref.earnings_total_kopeks / 100)} {currencySymbol}
)}
))}
{/* Period Stats */}
{t('adminDashboard.period.today')}
{formatAmount( (referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited ).reduce((sum, r) => sum + r.earnings_today_kopeks, 0) / 100, )}{' '} {currencySymbol}
{t('adminDashboard.period.week')}
{formatAmount( (referrersTab === 'earnings' ? referrers.by_earnings : referrers.by_invited ).reduce((sum, r) => sum + r.earnings_week_kopeks, 0) / 100, )}{' '} {currencySymbol}
{t('adminDashboard.period.month')}
{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 && (

{t('adminDashboard.topCampaigns.title')}

{campaigns.total_campaigns}{' '} {t('adminDashboard.topCampaigns.stats', { count: 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)}%
))}
{t('adminDashboard.topCampaigns.total')} {formatAmount(campaigns.total_revenue_kopeks / 100)} {currencySymbol}
)}
{/* Recent Payments */} {payments && payments.payments.length > 0 && (

{t('adminDashboard.recentPayments.title')}

{t('adminDashboard.recentPayments.today', { amount: `${formatAmount(payments.total_today_kopeks / 100)} ${currencySymbol}`, })} {' '} ·{' '} {t('adminDashboard.recentPayments.week', { amount: `${formatAmount(payments.total_week_kopeks / 100)} ${currencySymbol}`, })}

{/* Desktop Table */}
{payments.payments.slice(0, 10).map((payment) => ( ))}
{t('adminDashboard.table.user')} {t('adminDashboard.table.type')} {t('adminDashboard.table.amount')} {t('adminDashboard.table.method')} {t('adminDashboard.table.date')}
{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', })}
))}
)}
); }