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')}
|
{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) && (
{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 */}
|
{t('adminDashboard.table.user')}
|
{t('adminDashboard.table.type')}
|
{t('adminDashboard.table.amount')}
|
{t('adminDashboard.table.method')}
|
{t('adminDashboard.table.date')}
|
{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',
})}
))}
)}
);
}