import { useState, useEffect, useCallback } from 'react' import { useCurrency } from '../hooks/useCurrency' import { adminUsersApi, type UserListItem, type UserDetailResponse, type UsersStatsResponse, type UserAvailableTariff, type PanelSyncStatusResponse, } from '../api/adminUsers' // ============ Icons ============ const UsersIcon = ({ className = "w-6 h-6" }: { className?: string }) => ( ) const SearchIcon = () => ( ) const ChevronLeftIcon = () => ( ) const ChevronRightIcon = () => ( ) const XMarkIcon = () => ( ) const RefreshIcon = ({ className = "w-5 h-5" }: { className?: string }) => ( ) const PlusIcon = () => ( ) const MinusIcon = () => ( ) const SyncIcon = ({ className = "w-5 h-5" }: { className?: string }) => ( ) const TelegramIcon = () => ( ) // ============ Components ============ interface StatCardProps { title: string value: string | number subtitle?: string color: 'blue' | 'green' | 'yellow' | 'red' | 'purple' } function StatCard({ title, value, subtitle, color }: StatCardProps) { const colors = { blue: 'bg-blue-500/20 text-blue-400 border-blue-500/30', green: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', yellow: 'bg-amber-500/20 text-amber-400 border-amber-500/30', red: 'bg-rose-500/20 text-rose-400 border-rose-500/30', purple: 'bg-purple-500/20 text-purple-400 border-purple-500/30', } return (
{value}
{title}
{subtitle &&
{subtitle}
}
) } function StatusBadge({ status }: { status: string }) { const styles: Record = { active: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', blocked: 'bg-rose-500/20 text-rose-400 border-rose-500/30', deleted: 'bg-dark-600 text-dark-400 border-dark-500', trial: 'bg-blue-500/20 text-blue-400 border-blue-500/30', expired: 'bg-amber-500/20 text-amber-400 border-amber-500/30', disabled: 'bg-dark-600 text-dark-400 border-dark-500', } return ( {status} ) } // ============ User List Component ============ interface UserRowProps { user: UserListItem onSelect: (user: UserListItem) => void formatAmount: (rubAmount: number) => string } function UserRow({ user, onSelect, formatAmount }: UserRowProps) { return (
onSelect(user)} className="flex items-center gap-4 p-4 bg-dark-800/50 rounded-xl border border-dark-700 hover:border-dark-600 hover:bg-dark-800 cursor-pointer transition-all" > {/* Avatar */}
{user.first_name?.[0] || user.username?.[0] || '?'}
{/* Info */}
{user.full_name} {user.username && ( @{user.username} )}
{user.telegram_id} {user.subscription_status && ( )}
{/* Balance */}
{formatAmount(user.balance_rubles)}
{user.purchase_count > 0 ? `${user.purchase_count} покупок` : 'Нет покупок'}
) } // ============ User Detail Modal ============ interface UserDetailModalProps { userId: number onClose: () => void onUpdate: () => void } function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) { const { formatWithCurrency } = useCurrency() 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 loadUser = useCallback(async () => { try { setLoading(true) const data = await adminUsersApi.getUser(userId) setUser(data) } catch (error) { console.error('Failed to load user:', error) } finally { setLoading(false) } }, [userId]) const loadSyncStatus = useCallback(async () => { try { const data = await adminUsersApi.getSyncStatus(userId) setSyncStatus(data) } catch (error) { console.error('Failed to load sync status:', error) } }, [userId]) const loadTariffs = useCallback(async () => { try { const data = await adminUsersApi.getAvailableTariffs(userId, true) setTariffs(data.tariffs) } catch (error) { console.error('Failed to load tariffs:', error) } }, [userId]) useEffect(() => { loadUser() }, [loadUser]) useEffect(() => { if (activeTab === 'sync') loadSyncStatus() if (activeTab === 'subscription') loadTariffs() }, [activeTab, loadSyncStatus, loadTariffs]) const handleUpdateBalance = async (isAdd: boolean) => { if (!balanceAmount) return setActionLoading(true) try { const amount = Math.abs(parseFloat(balanceAmount) * 100) await adminUsersApi.updateBalance(userId, { amount_kopeks: isAdd ? amount : -amount, description: balanceDescription || (isAdd ? 'Начисление админом' : 'Списание админом'), }) await loadUser() setBalanceAmount('') setBalanceDescription('') onUpdate() } catch (error) { console.error('Failed to update balance:', error) } finally { setActionLoading(false) } } const handleUpdateSubscription = async () => { setActionLoading(true) try { const data: Record = { action: subAction } if (subAction === 'extend') data.days = parseInt(subDays) if (subAction === 'change_tariff' && selectedTariffId) data.tariff_id = selectedTariffId if (subAction === 'create') { data.days = parseInt(subDays) if (selectedTariffId) data.tariff_id = selectedTariffId } await adminUsersApi.updateSubscription(userId, data as any) await loadUser() onUpdate() } catch (error) { console.error('Failed to update subscription:', error) } finally { setActionLoading(false) } } const handleBlockUser = async () => { if (!confirm('Заблокировать пользователя?')) return setActionLoading(true) try { await adminUsersApi.blockUser(userId) await loadUser() onUpdate() } catch (error) { console.error('Failed to block user:', error) } finally { setActionLoading(false) } } const handleUnblockUser = async () => { setActionLoading(true) try { await adminUsersApi.unblockUser(userId) await loadUser() onUpdate() } catch (error) { console.error('Failed to unblock user:', error) } finally { setActionLoading(false) } } const handleSyncFromPanel = async () => { setActionLoading(true) try { await adminUsersApi.syncFromPanel(userId, { update_subscription: true, update_traffic: true }) await loadUser() await loadSyncStatus() onUpdate() } catch (error) { console.error('Failed to sync from panel:', error) } finally { setActionLoading(false) } } const handleSyncToPanel = async () => { setActionLoading(true) try { await adminUsersApi.syncToPanel(userId, { create_if_missing: true }) await loadUser() await loadSyncStatus() onUpdate() } 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('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } if (loading) { return (
) } if (!user) return null 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 */}
Статус
{user.status === 'active' ? ( ) : user.status === 'blocked' ? ( ) : null}
{/* Details grid */}
Email
{user.email || '-'}
Язык
{user.language}
Регистрация
{formatDate(user.created_at)}
Последняя активность
{formatDate(user.last_activity)}
Всего потрачено
{formatWithCurrency(user.total_spent_kopeks / 100)}
Покупок
{user.purchase_count}
{/* Referral */}
Реферальная программа
{user.referral.referrals_count}
Рефералов
{formatWithCurrency(user.referral.total_earnings_kopeks / 100)}
Заработано
{user.referral.commission_percent || 0}%
Комиссия
{/* Restrictions */} {(user.restriction_topup || user.restriction_subscription) && (
Ограничения
{user.restriction_topup &&
• Запрет пополнения
} {user.restriction_subscription &&
• Запрет покупки подписки
} {user.restriction_reason &&
Причина: {user.restriction_reason}
}
)}
)} {/* Subscription Tab */} {activeTab === 'subscription' && (
{user.subscription ? ( <> {/* Current subscription */}
Текущая подписка
Тариф
{user.subscription.tariff_name || 'Не указан'}
Действует до
{formatDate(user.subscription.end_date)}
Трафик
{user.subscription.traffic_used_gb.toFixed(1)} / {user.subscription.traffic_limit_gb} ГБ
Устройств
{user.subscription.device_limit}
{/* Actions */}
Действия
{subAction === 'extend' && ( setSubDays(e.target.value)} placeholder="Дней" className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-dark-100" /> )} {subAction === 'change_tariff' && ( )}
) : (
Нет активной подписки
setSubDays(e.target.value)} placeholder="Дней" className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-dark-100" />
)}
)} {/* Balance Tab */} {activeTab === 'balance' && (
{/* Current balance */}
Текущий баланс
{formatWithCurrency(user.balance_rubles)}
{/* Add/subtract form */}
setBalanceAmount(e.target.value)} placeholder="Сумма в рублях" className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-dark-100" /> setBalanceDescription(e.target.value)} placeholder="Описание (опционально)" className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-dark-100" />
{/* Recent transactions */} {user.recent_transactions.length > 0 && (
Последние транзакции
{user.recent_transactions.map((tx) => (
{tx.description || tx.type}
{formatDate(tx.created_at)}
= 0 ? 'text-emerald-400' : 'text-rose-400'}> {tx.amount_kopeks >= 0 ? '+' : ''}{formatWithCurrency(tx.amount_rubles)}
))}
)}
)} {/* Sync Tab */} {activeTab === 'sync' && (
{/* Sync status */} {syncStatus && (
{syncStatus.has_differences ? ( Есть расхождения ) : ( Синхронизировано )}
{syncStatus.differences.length > 0 && (
{syncStatus.differences.map((diff, i) => (
• {diff}
))}
)}
Бот
Статус: {syncStatus.bot_subscription_status || '-'}
Трафик: {syncStatus.bot_traffic_used_gb.toFixed(2)} ГБ
Панель
Статус: {syncStatus.panel_status || '-'}
Трафик: {syncStatus.panel_traffic_used_gb.toFixed(2)} ГБ
)} {/* UUID info */}
Remnawave UUID
{user.remnawave_uuid || 'Не привязан'}
{/* Sync buttons */}
)}
) } // ============ Main Page ============ export default function AdminUsers() { const { formatWithCurrency } = useCurrency() const [users, setUsers] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [sortBy, setSortBy] = useState('created_at') const [offset, setOffset] = useState(0) const [total, setTotal] = useState(0) const [selectedUserId, setSelectedUserId] = useState(null) const limit = 20 const loadUsers = useCallback(async () => { try { setLoading(true) const params: Record = { offset, limit, sort_by: sortBy } if (search) params.search = search if (statusFilter) params.status = statusFilter const data = await adminUsersApi.getUsers(params as any) setUsers(data.users) setTotal(data.total) } catch (error) { console.error('Failed to load users:', error) } finally { setLoading(false) } }, [offset, search, statusFilter, sortBy]) const loadStats = useCallback(async () => { try { const data = await adminUsersApi.getStats() setStats(data) } catch (error) { console.error('Failed to load stats:', error) } }, []) useEffect(() => { loadUsers() }, [loadUsers]) useEffect(() => { loadStats() }, [loadStats]) const handleSearch = (e: React.FormEvent) => { e.preventDefault() setOffset(0) loadUsers() } const totalPages = Math.ceil(total / limit) const currentPage = Math.floor(offset / limit) + 1 return (
{/* Header */}

Пользователи

Управление пользователями бота

{/* Stats */} {stats && (
)} {/* Filters */}
setSearch(e.target.value)} placeholder="Поиск по ID, имени, username..." className="w-full pl-10 pr-4 py-2 bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder-dark-500 focus:border-dark-600 focus:outline-none" style={{ paddingLeft: '2.5rem' }} />
{/* Users list */}
{loading ? (
) : users.length === 0 ? (
Пользователи не найдены
) : ( users.map((user) => ( setSelectedUserId(u.id)} formatAmount={(amount) => formatWithCurrency(amount)} /> )) )}
{/* Pagination */} {totalPages > 1 && (
Показано {offset + 1}-{Math.min(offset + limit, total)} из {total}
{currentPage} / {totalPages}
)} {/* User detail modal */} {selectedUserId && ( setSelectedUserId(null)} onUpdate={() => { loadUsers(); loadStats() }} /> )}
) }