import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin' import { useCurrency } from '../hooks/useCurrency' // Icons const ChartIcon = () => ( ) const ServerIcon = () => ( ) const UsersIcon = () => ( ) const CurrencyIcon = () => ( ) const SubscriptionIcon = () => ( ) const RefreshIcon = () => ( ) const PowerIcon = () => ( ) const RestartIcon = () => ( ) 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` } return (
{node.name}
{node.address}
{getStatusText()}
{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, index) => { 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 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) } } useEffect(() => { fetchStats() // Refresh every 30 seconds const interval = setInterval(fetchStats, 30000) return () => clearInterval(interval) }, []) 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 ? (
{stats.nodes.nodes.map((node) => ( ))}
) : (
{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)}%
)}
{/* 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 && (

{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}
)}
) }