import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
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
const BackIcon = () => (
)
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)
}, [])
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')}
| {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}
|
))}
)}
{/* 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'
})}
))}
)}
)
}