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 || '-'}
Регистрация
{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 */}
>
) : (
)}
)}
{/* Balance Tab */}
{activeTab === 'balance' && (
{/* Current balance */}
Текущий баланс
{formatWithCurrency(user.balance_rubles)}
{/* Add/subtract form */}
{/* 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 */}
{/* 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() }}
/>
)}
)
}