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')}
| {t('adminDashboard.tariffs.tariffName')} |
{t('adminDashboard.tariffs.activeSubscriptions')} |
{t('adminDashboard.tariffs.trialSubscriptions')} |
{t('adminDashboard.tariffs.purchasedToday')} |
{t('adminDashboard.tariffs.purchasedWeek')} |
{t('adminDashboard.tariffs.purchasedMonth')} |
{stats.tariff_stats.tariffs.map((tariff) => (
|
{tariff.tariff_name}
|
{tariff.active_subscriptions}
|
{tariff.trial_subscriptions}
|
{tariff.purchased_today}
|
{tariff.purchased_week}
|
{tariff.purchased_month}
|
))}
)}
)
}