import { useState, useEffect, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { useCurrency } from '../hooks/useCurrency'; import { useNotify } from '../platform/hooks/useNotify'; import { adminUsersApi, type UserDetailResponse, type UserAvailableTariff, type UserListItem, type UserPanelInfo, type UserNodeUsageResponse, type PanelSyncStatusResponse, type UpdateSubscriptionRequest, } from '../api/adminUsers'; import { adminApi, type AdminTicket, type AdminTicketDetail } from '../api/admin'; import { promocodesApi, type PromoGroup } from '../api/promocodes'; import { promoOffersApi } from '../api/promoOffers'; import { ticketsApi } from '../api/tickets'; import { AdminBackButton } from '../components/admin'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; // ============ Helpers ============ const getCountryFlag = (code: string | null | undefined): string => { if (!code) return ''; const codeMap: Record = { RU: '\u{1F1F7}\u{1F1FA}', US: '\u{1F1FA}\u{1F1F8}', DE: '\u{1F1E9}\u{1F1EA}', NL: '\u{1F1F3}\u{1F1F1}', GB: '\u{1F1EC}\u{1F1E7}', UK: '\u{1F1EC}\u{1F1E7}', FR: '\u{1F1EB}\u{1F1F7}', FI: '\u{1F1EB}\u{1F1EE}', SE: '\u{1F1F8}\u{1F1EA}', NO: '\u{1F1F3}\u{1F1F4}', PL: '\u{1F1F5}\u{1F1F1}', TR: '\u{1F1F9}\u{1F1F7}', JP: '\u{1F1EF}\u{1F1F5}', SG: '\u{1F1F8}\u{1F1EC}', HK: '\u{1F1ED}\u{1F1F0}', KR: '\u{1F1F0}\u{1F1F7}', AU: '\u{1F1E6}\u{1F1FA}', CA: '\u{1F1E8}\u{1F1E6}', CH: '\u{1F1E8}\u{1F1ED}', AT: '\u{1F1E6}\u{1F1F9}', IT: '\u{1F1EE}\u{1F1F9}', ES: '\u{1F1EA}\u{1F1F8}', BR: '\u{1F1E7}\u{1F1F7}', IN: '\u{1F1EE}\u{1F1F3}', AE: '\u{1F1E6}\u{1F1EA}', IL: '\u{1F1EE}\u{1F1F1}', KZ: '\u{1F1F0}\u{1F1FF}', UA: '\u{1F1FA}\u{1F1E6}', CZ: '\u{1F1E8}\u{1F1FF}', RO: '\u{1F1F7}\u{1F1F4}', LV: '\u{1F1F1}\u{1F1FB}', LT: '\u{1F1F1}\u{1F1F9}', EE: '\u{1F1EA}\u{1F1EA}', BG: '\u{1F1E7}\u{1F1EC}', HU: '\u{1F1ED}\u{1F1FA}', MD: '\u{1F1F2}\u{1F1E9}', }; return codeMap[code.toUpperCase()] || code; }; // ============ 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 notify = useNotify(); 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' | 'tickets' >('info'); const [syncStatus, setSyncStatus] = useState(null); const [tariffs, setTariffs] = useState([]); const [actionLoading, setActionLoading] = useState(false); // Referrals const [referrals, setReferrals] = useState([]); const [referralsLoading, setReferralsLoading] = useState(false); // Panel info & node usage const [panelInfo, setPanelInfo] = useState(null); const [panelInfoLoading, setPanelInfoLoading] = useState(false); const [nodeUsage, setNodeUsage] = useState(null); const [nodeUsageDays, setNodeUsageDays] = useState(7); // Inline confirm state const [confirmingAction, setConfirmingAction] = useState(null); const confirmTimerRef = useRef | null>(null); // Balance form const [balanceAmount, setBalanceAmount] = useState(''); const [balanceDescription, setBalanceDescription] = useState(''); // Tickets const [tickets, setTickets] = useState([]); const [ticketsLoading, setTicketsLoading] = useState(false); const [ticketsTotal, setTicketsTotal] = useState(0); const [selectedTicketId, setSelectedTicketId] = useState(null); const [selectedTicket, setSelectedTicket] = useState(null); const [ticketDetailLoading, setTicketDetailLoading] = useState(false); const [replyText, setReplyText] = useState(''); const [replySending, setReplySending] = useState(false); const messagesEndRef = useRef(null); // Subscription form const [subAction, setSubAction] = useState('extend'); const [subDays, setSubDays] = useState(30); const [selectedTariffId, setSelectedTariffId] = useState(null); // Promo group const [promoGroups, setPromoGroups] = useState([]); const [editingPromoGroup, setEditingPromoGroup] = useState(false); // Referral commission const [editingReferralCommission, setEditingReferralCommission] = useState(false); const [referralCommissionValue, setReferralCommissionValue] = useState(''); // Send promo offer const [offerDiscountPercent, setOfferDiscountPercent] = useState(''); const [offerValidHours, setOfferValidHours] = useState(24); const [offerSending, setOfferSending] = useState(false); // Traffic packages const [selectedTrafficGb, setSelectedTrafficGb] = useState(''); // Devices const [devices, setDevices] = useState< { hwid: string; platform: string; device_model: string; created_at: string | null }[] >([]); const [devicesTotal, setDevicesTotal] = useState(0); const [deviceLimit, setDeviceLimit] = useState(0); const [devicesLoading, setDevicesLoading] = useState(false); 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]); const loadTickets = useCallback(async () => { if (!userId) return; try { setTicketsLoading(true); const data = await adminApi.getTickets({ user_id: userId, per_page: 50 }); setTickets(data.items); setTicketsTotal(data.total); } catch (error) { console.error('Failed to load tickets:', error); } finally { setTicketsLoading(false); } }, [userId]); const loadTicketDetail = useCallback(async (ticketId: number) => { try { setTicketDetailLoading(true); const data = await adminApi.getTicket(ticketId); setSelectedTicket(data); } catch (error) { console.error('Failed to load ticket detail:', error); } finally { setTicketDetailLoading(false); } }, []); const loadReferrals = useCallback(async () => { if (!userId) return; try { setReferralsLoading(true); const data = await adminUsersApi.getReferrals(userId, 0, 50); setReferrals(data.users); } catch { // ignore } finally { setReferralsLoading(false); } }, [userId]); const loadPanelInfo = useCallback(async () => { if (!userId) return; try { setPanelInfoLoading(true); const data = await adminUsersApi.getPanelInfo(userId); setPanelInfo(data); } catch { // ignore } finally { setPanelInfoLoading(false); } }, [userId]); const loadNodeUsage = useCallback(async () => { if (!userId) return; try { const data = await adminUsersApi.getNodeUsage(userId); setNodeUsage(data); } catch { // ignore } }, [userId]); const loadDevices = useCallback(async () => { if (!userId) return; try { setDevicesLoading(true); const data = await adminUsersApi.getUserDevices(userId); setDevices(data.devices); setDevicesTotal(data.total); setDeviceLimit(data.device_limit); } catch { // ignore } finally { setDevicesLoading(false); } }, [userId]); const loadSubscriptionData = useCallback(async () => { await Promise.all([loadPanelInfo(), loadNodeUsage(), loadDevices()]); }, [loadPanelInfo, loadNodeUsage, loadDevices]); const loadPromoGroups = useCallback(async () => { try { const data = await promocodesApi.getPromoGroups({ limit: 100 }); setPromoGroups(data.items); } catch { // ignore } }, []); const handleTicketReply = async () => { if (!selectedTicketId || !replyText.trim()) return; setReplySending(true); try { await adminApi.replyToTicket(selectedTicketId, replyText); setReplyText(''); await loadTicketDetail(selectedTicketId); await loadTickets(); } catch (error) { console.error('Failed to reply:', error); } finally { setReplySending(false); } }; const handleTicketStatusChange = async (newStatus: string) => { if (!selectedTicketId) return; setActionLoading(true); try { await adminApi.updateTicketStatus(selectedTicketId, newStatus); await loadTicketDetail(selectedTicketId); await loadTickets(); } catch (error) { console.error('Failed to update ticket status:', error); } finally { setActionLoading(false); } }; useEffect(() => { if (selectedTicketId) { loadTicketDetail(selectedTicketId); } }, [selectedTicketId, loadTicketDetail]); useEffect(() => { if (selectedTicket && messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [selectedTicket]); useEffect(() => { if (!userId || isNaN(userId)) { navigate('/admin/users'); return; } loadUser(); }, [userId, loadUser, navigate]); useEffect(() => { if (activeTab === 'info') { loadReferrals(); loadPromoGroups(); } if (activeTab === 'sync') loadSyncStatus(); if (activeTab === 'subscription') { loadTariffs(); loadSubscriptionData(); } if (activeTab === 'tickets') loadTickets(); }, [ activeTab, loadSyncStatus, loadTariffs, loadTickets, loadReferrals, loadSubscriptionData, loadPromoGroups, ]); const handleUpdateBalance = async (isAdd: boolean) => { if (balanceAmount === '' || !userId) return; setActionLoading(true); try { const amount = Math.abs(toNumber(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 (overrideAction?: string) => { if (!userId) return; setActionLoading(true); try { const action = overrideAction || subAction; const data: UpdateSubscriptionRequest = { action: action as UpdateSubscriptionRequest['action'], ...(action === 'extend' ? { days: toNumber(subDays, 30) } : {}), ...(action === 'change_tariff' && selectedTariffId ? { tariff_id: selectedTariffId } : {}), ...(action === 'create' ? { days: toNumber(subDays, 30), ...(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 handleInlineConfirm = (actionKey: string, executeFn: () => Promise) => { if (confirmingAction === actionKey) { if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); setConfirmingAction(null); executeFn().catch(() => {}); } else { if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); setConfirmingAction(actionKey); confirmTimerRef.current = setTimeout(() => setConfirmingAction(null), 3000); } }; const handleDeleteDevice = async (hwid: string) => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.deleteUserDevice(userId, hwid); notify.success(t('admin.users.detail.devices.deleted')); await loadDevices(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleResetDevices = async () => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.resetUserDevices(userId); notify.success(t('admin.users.detail.devices.allDeleted')); await loadDevices(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleAddTraffic = async (gb: number) => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.updateSubscription(userId, { action: 'add_traffic', traffic_gb: gb }); notify.success(t('admin.users.detail.subscription.trafficAdded')); setSelectedTrafficGb(''); await loadUser(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleRemoveTraffic = async (purchaseId: number) => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.updateSubscription(userId, { action: 'remove_traffic', traffic_purchase_id: purchaseId, }); notify.success(t('admin.users.detail.subscription.trafficRemoved')); await loadUser(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleSetDeviceLimit = async (newLimit: number) => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.updateSubscription(userId, { action: 'set_device_limit', device_limit: newLimit, }); notify.success(t('admin.users.detail.subscription.deviceLimitUpdated')); await loadUser(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const currentTariff = tariffs.find((t) => t.id === user?.subscription?.tariff_id) || null; const handleChangePromoGroup = async (groupId: number | null) => { if (!userId) return; setActionLoading(true); try { await adminUsersApi.updatePromoGroup(userId, groupId); await loadUser(); setEditingPromoGroup(false); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleUpdateReferralCommission = async () => { if (!userId) return; setActionLoading(true); try { const value = referralCommissionValue === '' ? null : toNumber(referralCommissionValue); if (value !== null && (value < 0 || value > 100)) { notify.error(t('admin.users.detail.referral.invalidPercent'), t('common.error')); return; } await adminUsersApi.updateReferralCommission(userId, value); await loadUser(); setEditingReferralCommission(false); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleDeactivateOffer = async () => { if (!userId) return; setActionLoading(true); try { await promocodesApi.deactivateDiscount(userId); notify.success(t('admin.users.detail.offerDeactivated'), t('common.success')); await loadUser(); } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleSendOffer = async () => { if (!userId || offerDiscountPercent === '' || offerValidHours === '') return; setOfferSending(true); try { await promoOffersApi.broadcastOffer({ user_id: userId, notification_type: 'admin_personal', discount_percent: toNumber(offerDiscountPercent), valid_hours: toNumber(offerValidHours, 24), effect_type: 'percent_discount', send_notification: true, }); notify.success(t('admin.users.detail.offerSent'), t('common.success')); setOfferDiscountPercent(''); setOfferValidHours(24); await loadUser(); } catch { notify.error(t('admin.users.detail.offerSendError'), t('common.error')); } finally { setOfferSending(false); } }; const handleResetTrial = async () => { if (!userId) return; setActionLoading(true); try { const result = await adminUsersApi.resetTrial(userId); if (result.success) { notify.success(t('admin.users.userActions.success.resetTrial'), t('common.success')); await loadUser(); } else { notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); } } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleResetSubscription = async () => { if (!userId) return; setActionLoading(true); try { const result = await adminUsersApi.resetSubscription(userId); if (result.success) { notify.success(t('admin.users.userActions.success.resetSubscription'), t('common.success')); await loadUser(); } else { notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); } } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleDisableUser = async () => { if (!userId) return; setActionLoading(true); try { const result = await adminUsersApi.disableUser(userId); if (result.success) { notify.success(t('admin.users.userActions.success.disable'), t('common.success')); await loadUser(); } else { notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); } } catch { notify.error(t('admin.users.userActions.error'), t('common.error')); } finally { setActionLoading(false); } }; const handleFullDeleteUser = async () => { if (!userId) return; setActionLoading(true); try { const result = await adminUsersApi.fullDeleteUser(userId); if (result.success) { notify.success(t('admin.users.userActions.success.delete'), t('common.success')); navigate('/admin/users'); } else { notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); } } catch { notify.error(t('admin.users.userActions.error'), t('common.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', }); }; // Compute node usage for selected period from cached 30-day data const nodeUsageForPeriod = (() => { if (!nodeUsage || nodeUsage.items.length === 0) return []; return nodeUsage.items .map((item) => { const daily = item.daily_bytes || []; const sliced = daily.slice(-nodeUsageDays); const total = sliced.reduce((sum, v) => sum + v, 0); return { ...item, total_bytes: total }; }) .sort((a, b) => b.total_bytes - a.total_bytes); })(); const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; }; const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); notify.success(t('admin.users.detail.copied')); } catch { // ignore } }; 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', 'tickets'] 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}
{/* Campaign */} {user.campaign_name && (
{t('admin.users.detail.campaign')}
{user.campaign_name}
)} {/* Promo Group */}
{t('admin.users.detail.promoGroup')}
{editingPromoGroup ? (
{user.promo_group && ( )}
) : (
{user.promo_group?.name || ( {t('admin.users.detail.noPromoGroup')} )}
)}
{/* 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')}
{editingReferralCommission ? (
) : ( <>
{user.referral.commission_percent != null ? `${user.referral.commission_percent}%` : t('admin.users.detail.referral.default')}
{t('admin.users.detail.referral.commission')}
)}
{/* Referrals list */} {user.referral.referrals_count > 0 && (
{t('admin.users.detail.referralsList')}
{referralsLoading ? (
) : referrals.length === 0 ? (
{t('admin.users.detail.noReferrals')}
) : (
{referrals.map((ref) => ( ))}
)}
)} {/* 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}
)}
)} {/* Actions */}
{t('admin.users.detail.actions.title')}
)} {/* 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}
{/* Traffic Packages */} {user.subscription.traffic_purchases && user.subscription.traffic_purchases.length > 0 && (
{t('admin.users.detail.subscription.trafficPackages')} {user.subscription.purchased_traffic_gb > 0 && ( ({user.subscription.purchased_traffic_gb} {t('common.units.gb')}) )}
{user.subscription.traffic_purchases.map((tp) => (
{tp.traffic_gb} {t('common.units.gb')} {tp.is_expired ? ( {t('admin.users.detail.subscription.expired')} ) : ( {tp.days_remaining}{' '} {t('admin.users.detail.subscription.daysLeft')} )}
{!tp.is_expired && ( )}
))}
)} {/* Add Traffic */} {currentTariff && currentTariff.traffic_topup_enabled && Object.keys(currentTariff.traffic_topup_packages).length > 0 && (
{t('admin.users.detail.subscription.addTraffic')}
{t('admin.users.detail.subscription.addTrafficNote')}
)} {/* Actions */}
{t('admin.users.detail.subscription.actions')}
{subAction === 'extend' && ( )} {subAction === 'change_tariff' && ( )}
) : (
{t('admin.users.detail.subscription.noActive')}
)} {/* Panel Info */} {panelInfoLoading ? (
) : panelInfo && !panelInfo.found ? (
{t('admin.users.detail.panelNotFound')}
) : panelInfo && panelInfo.found ? ( <> {/* Links */} {(panelInfo.subscription_url || panelInfo.happ_link) && (
{t('admin.users.detail.subscriptionUrl')} / {t('admin.users.detail.happLink')}
{panelInfo.subscription_url && ( )} {panelInfo.happ_link && ( )}
)} {/* Config */} {(panelInfo.trojan_password || panelInfo.vless_uuid || panelInfo.ss_password) && (
{t('admin.users.detail.panelConfig')}
{panelInfo.trojan_password && ( )} {panelInfo.vless_uuid && ( )} {panelInfo.ss_password && ( )}
)} {/* Connection info */}
{t('admin.users.detail.firstConnected')}
{formatDate(panelInfo.first_connected_at)}
{t('admin.users.detail.lastOnline')}
{formatDate(panelInfo.online_at)}
{panelInfo.last_connected_node_name && (
{t('admin.users.detail.lastNode')}
{panelInfo.last_connected_node_name}
)}
{/* Live traffic */}
{t('admin.users.detail.liveTraffic')}
{formatBytes(panelInfo.used_traffic_bytes)} {panelInfo.traffic_limit_bytes > 0 ? formatBytes(panelInfo.traffic_limit_bytes) : '∞'}
0 ? `${Math.min(100, (panelInfo.used_traffic_bytes / panelInfo.traffic_limit_bytes) * 100)}%` : '0%', }} />
{t('admin.users.detail.lifetime')}:{' '} {formatBytes(panelInfo.lifetime_used_traffic_bytes)}
{/* Node usage */}
{t('admin.users.detail.nodeUsage')}
{[1, 3, 7, 14, 30].map((d) => ( ))}
{nodeUsageForPeriod.length > 0 ? (
{nodeUsageForPeriod.map((item) => { const maxBytes = nodeUsageForPeriod[0].total_bytes; const pct = maxBytes > 0 ? (item.total_bytes / maxBytes) * 100 : 0; return (
{item.country_code && ( {getCountryFlag(item.country_code)} )} {item.node_name} {formatBytes(item.total_bytes)}
); })}
) : (
-
)}
) : null} {/* Devices */}
{t('admin.users.detail.devices.title')} ({devicesTotal}/{deviceLimit})
{devices.length > 0 && ( )}
{devicesLoading ? (
) : devices.length > 0 ? (
{devices.map((device) => (
{device.platform || device.device_model || device.hwid.slice(0, 12)}
{device.device_model && device.platform && ( {device.device_model} )} {device.hwid.slice(0, 8)}... {device.created_at && ( {new Date(device.created_at).toLocaleDateString(locale)} )}
))}
) : (
{t('admin.users.detail.devices.none')}
)}
)} {/* Balance Tab */} {activeTab === 'balance' && (
{/* Current balance */}
{t('admin.users.detail.balance.current')}
{formatWithCurrency(user.balance_rubles)}
{/* Add/subtract form */}
setBalanceDescription(e.target.value)} placeholder={t('admin.users.detail.balance.descriptionPlaceholder')} className="input" maxLength={500} />
{/* Active promo offer */} {user.promo_offer_discount_percent > 0 && (
{t('admin.users.detail.activePromoOffer')}
{user.promo_offer_discount_percent}%
{t('admin.users.detail.discount')}
{user.promo_offer_discount_source || '-'}
{t('admin.users.detail.source')}
{user.promo_offer_discount_expires_at ? formatDate(user.promo_offer_discount_expires_at) : '-'}
{t('admin.users.detail.expiresAt')}
)} {/* Send promo offer */}
{t('admin.users.detail.sendOffer')}
{/* 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 */}
)} {/* Tickets Tab */} {activeTab === 'tickets' && (
{selectedTicketId ? ( /* Ticket Chat View */ ticketDetailLoading ? (
) : selectedTicket ? (
{/* Chat header */}
#{selectedTicket.id} {selectedTicket.title}
{selectedTicket.status} {formatDate(selectedTicket.created_at)}
{/* Status buttons */}
{(['open', 'pending', 'answered', 'closed'] as const).map((s) => ( ))}
{/* Messages */}
{selectedTicket.messages.map((msg) => (
{msg.is_from_admin ? t('admin.tickets.adminLabel') : t('admin.tickets.userLabel')} {formatDate(msg.created_at)}

{msg.message_text}

{msg.has_media && msg.media_file_id && (
{msg.media_type === 'photo' ? ( {msg.media_caption ) : ( {msg.media_caption || msg.media_type} )} {msg.media_caption && msg.media_type === 'photo' && (

{msg.media_caption}

)}
)}
))}
{/* Reply form */} {selectedTicket.status !== 'closed' && (