import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { useCurrency } from '../hooks/useCurrency'; import { adminUsersApi, type UserDetailResponse, type UserAvailableTariff, type PanelSyncStatusResponse, type UpdateSubscriptionRequest, } from '../api/adminUsers'; import { AdminBackButton } from '../components/admin'; // ============ Icons ============ const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const PlusIcon = () => ( ); const MinusIcon = () => ( ); const ArrowDownIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const ArrowUpIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const TelegramIcon = () => ( ); // ============ Components ============ function StatusBadge({ status }: { status: string }) { const styles: Record = { active: 'bg-success-500/20 text-success-400 border-success-500/30', blocked: 'bg-error-500/20 text-error-400 border-error-500/30', deleted: 'bg-dark-600 text-dark-400 border-dark-500', trial: 'bg-accent-500/20 text-accent-400 border-accent-500/30', expired: 'bg-warning-500/20 text-warning-400 border-warning-500/30', disabled: 'bg-dark-600 text-dark-400 border-dark-500', }; return ( {status} ); } // ============ Main Page ============ export default function AdminUserDetail() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; const locale = localeMap[i18n.language] || 'ru-RU'; const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'info' | 'subscription' | 'balance' | 'sync'>('info'); const [syncStatus, setSyncStatus] = useState(null); const [tariffs, setTariffs] = useState([]); const [actionLoading, setActionLoading] = useState(false); // Balance form const [balanceAmount, setBalanceAmount] = useState(''); const [balanceDescription, setBalanceDescription] = useState(''); // Subscription form const [subAction, setSubAction] = useState('extend'); const [subDays, setSubDays] = useState('30'); const [selectedTariffId, setSelectedTariffId] = useState(null); const userId = id ? parseInt(id, 10) : null; const loadUser = useCallback(async () => { if (!userId) return; try { setLoading(true); const data = await adminUsersApi.getUser(userId); setUser(data); } catch (error) { console.error('Failed to load user:', error); navigate('/admin/users'); } finally { setLoading(false); } }, [userId, navigate]); const loadSyncStatus = useCallback(async () => { if (!userId) return; try { const data = await adminUsersApi.getSyncStatus(userId); setSyncStatus(data); } catch (error) { console.error('Failed to load sync status:', error); } }, [userId]); const loadTariffs = useCallback(async () => { if (!userId) return; try { const data = await adminUsersApi.getAvailableTariffs(userId, true); setTariffs(data.tariffs); } catch (error) { console.error('Failed to load tariffs:', error); } }, [userId]); useEffect(() => { if (!userId || isNaN(userId)) { navigate('/admin/users'); return; } loadUser(); }, [userId, loadUser, navigate]); useEffect(() => { if (activeTab === 'sync') loadSyncStatus(); if (activeTab === 'subscription') loadTariffs(); }, [activeTab, loadSyncStatus, loadTariffs]); const handleUpdateBalance = async (isAdd: boolean) => { if (!balanceAmount || !userId) return; setActionLoading(true); try { const amount = Math.abs(parseFloat(balanceAmount) * 100); await adminUsersApi.updateBalance(userId, { amount_kopeks: isAdd ? amount : -amount, description: balanceDescription || (isAdd ? t('admin.users.detail.balance.addByAdmin') : t('admin.users.detail.balance.subtractByAdmin')), }); await loadUser(); setBalanceAmount(''); setBalanceDescription(''); } catch (error) { console.error('Failed to update balance:', error); } finally { setActionLoading(false); } }; const handleUpdateSubscription = async () => { if (!userId) return; setActionLoading(true); try { const data: UpdateSubscriptionRequest = { action: subAction as UpdateSubscriptionRequest['action'], ...(subAction === 'extend' ? { days: parseInt(subDays) } : {}), ...(subAction === 'change_tariff' && selectedTariffId ? { tariff_id: selectedTariffId } : {}), ...(subAction === 'create' ? { days: parseInt(subDays), ...(selectedTariffId ? { tariff_id: selectedTariffId } : {}), } : {}), }; await adminUsersApi.updateSubscription(userId, data); await loadUser(); } catch (error) { console.error('Failed to update subscription:', error); } finally { setActionLoading(false); } }; const handleBlockUser = async () => { if (!userId || !confirm(t('admin.users.confirm.block'))) return; setActionLoading(true); try { await adminUsersApi.blockUser(userId); await loadUser(); } catch (error) { console.error('Failed to block user:', error); } finally { setActionLoading(false); } }; const handleUnblockUser = async () => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.unblockUser(userId); await loadUser(); } catch (error) { console.error('Failed to unblock user:', error); } finally { setActionLoading(false); } }; const handleSyncFromPanel = async () => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.syncFromPanel(userId, { update_subscription: true, update_traffic: true, }); await loadUser(); await loadSyncStatus(); } catch (error) { console.error('Failed to sync from panel:', error); } finally { setActionLoading(false); } }; const handleSyncToPanel = async () => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.syncToPanel(userId, { create_if_missing: true }); await loadUser(); await loadSyncStatus(); } catch (error) { console.error('Failed to sync to panel:', error); } finally { setActionLoading(false); } }; const formatDate = (date: string | null) => { if (!date) return '-'; return new Date(date).toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; if (loading) { return (
); } if (!user) { return (

{t('admin.users.notFound')}

); } return (
{/* Header */}
{user.first_name?.[0] || user.username?.[0] || '?'}
{user.full_name}
{user.telegram_id} {user.username && @{user.username}}
{/* Tabs */}
{(['info', 'subscription', 'balance', 'sync'] as const).map((tab) => ( ))}
{/* Content */}
{/* Info Tab */} {activeTab === 'info' && (
{/* Status */}
{t('admin.users.detail.status')}
{user.status === 'active' ? ( ) : user.status === 'blocked' ? ( ) : null}
{/* Details grid */}
Email
{user.email || '-'}
{t('admin.users.detail.language')}
{user.language}
{t('admin.users.detail.registration')}
{formatDate(user.created_at)}
{t('admin.users.detail.lastActivity')}
{formatDate(user.last_activity)}
{t('admin.users.detail.totalSpent')}
{formatWithCurrency(user.total_spent_kopeks / 100)}
{t('admin.users.detail.purchases')}
{user.purchase_count}
{/* Referral */}
{t('admin.users.detail.referral.title')}
{user.referral.referrals_count}
{t('admin.users.detail.referral.referrals')}
{formatWithCurrency(user.referral.total_earnings_kopeks / 100)}
{t('admin.users.detail.referral.earned')}
{user.referral.commission_percent || 0}%
{t('admin.users.detail.referral.commission')}
{/* Restrictions */} {(user.restriction_topup || user.restriction_subscription) && (
{t('admin.users.detail.restrictions.title')}
{user.restriction_topup && (
{t('admin.users.detail.restrictions.topup')}
)} {user.restriction_subscription && (
{t('admin.users.detail.restrictions.subscription')}
)} {user.restriction_reason && (
{t('admin.users.detail.restrictions.reason')}: {user.restriction_reason}
)}
)}
)} {/* Subscription Tab */} {activeTab === 'subscription' && (
{user.subscription ? ( <> {/* Current subscription */}
{t('admin.users.detail.subscription.current')}
{t('admin.users.detail.subscription.tariff')}
{user.subscription.tariff_name || t('admin.users.detail.subscription.notSpecified')}
{t('admin.users.detail.subscription.validUntil')}
{formatDate(user.subscription.end_date)}
{t('admin.users.detail.subscription.traffic')}
{user.subscription.traffic_used_gb.toFixed(1)} /{' '} {user.subscription.traffic_limit_gb} {t('common.units.gb')}
{t('admin.users.detail.subscription.devices')}
{user.subscription.device_limit}
{/* Actions */}
{t('admin.users.detail.subscription.actions')}
{subAction === 'extend' && ( setSubDays(e.target.value)} placeholder={t('admin.users.detail.subscription.days')} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100" /> )} {subAction === 'change_tariff' && ( )}
) : (
{t('admin.users.detail.subscription.noActive')}
setSubDays(e.target.value)} placeholder={t('admin.users.detail.subscription.days')} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100" />
)}
)} {/* Balance Tab */} {activeTab === 'balance' && (
{/* Current balance */}
{t('admin.users.detail.balance.current')}
{formatWithCurrency(user.balance_rubles)}
{/* Add/subtract form */}
setBalanceAmount(e.target.value)} placeholder={t('admin.users.detail.balance.amountPlaceholder')} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100" /> setBalanceDescription(e.target.value)} placeholder={t('admin.users.detail.balance.descriptionPlaceholder')} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100" />
{/* Recent transactions */} {user.recent_transactions.length > 0 && (
{t('admin.users.detail.balance.recentTransactions')}
{user.recent_transactions.map((tx) => (
{tx.description || tx.type}
{formatDate(tx.created_at)}
= 0 ? 'text-success-400' : 'text-error-400'} > {tx.amount_kopeks >= 0 ? '+' : ''} {formatWithCurrency(tx.amount_rubles)}
))}
)}
)} {/* Sync Tab */} {activeTab === 'sync' && (
{/* Sync status */} {syncStatus && (
{syncStatus.has_differences ? ( {t('admin.users.detail.sync.hasDifferences')} ) : ( {t('admin.users.detail.sync.synced')} )}
{syncStatus.differences.length > 0 && (
{syncStatus.differences.map((diff, i) => (
• {diff}
))}
)}
{t('admin.users.detail.sync.bot')}
{t('admin.users.detail.sync.statusLabel')}: {syncStatus.bot_subscription_status || '-'}
{t('admin.users.detail.sync.until')}: {syncStatus.bot_subscription_end_date ? new Date(syncStatus.bot_subscription_end_date).toLocaleDateString( locale, ) : '-'}
{t('admin.users.detail.sync.traffic')}: {syncStatus.bot_traffic_used_gb.toFixed(2)} {t('common.units.gb')}
{t('admin.users.detail.sync.devices')}: {syncStatus.bot_device_limit}
{t('admin.users.detail.sync.squads')}: {syncStatus.bot_squads?.length || 0}
{t('admin.users.detail.sync.panel')}
{t('admin.users.detail.sync.statusLabel')}: {syncStatus.panel_status || '-'}
{t('admin.users.detail.sync.until')}: {syncStatus.panel_expire_at ? new Date(syncStatus.panel_expire_at).toLocaleDateString(locale) : '-'}
{t('admin.users.detail.sync.traffic')}: {syncStatus.panel_traffic_used_gb.toFixed(2)} {t('common.units.gb')}
{t('admin.users.detail.sync.devices')}: {syncStatus.panel_device_limit}
{t('admin.users.detail.sync.squads')}: {syncStatus.panel_squads?.length || 0}
)} {/* UUID info */}
Remnawave UUID
{syncStatus?.remnawave_uuid || user.remnawave_uuid || t('admin.users.detail.sync.notLinked')}
{/* Sync buttons */}
)}
); }