import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useCurrency } from '../hooks/useCurrency'; import { useToast } from '../components/Toast'; import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers'; import { AdminBackButton } from '../components/admin'; // ============ Icons ============ const SearchIcon = () => ( ); const ChevronLeftIcon = () => ( ); const ChevronRightIcon = () => ( ); const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const TelegramIcon = () => ( ); const DotsVerticalIcon = () => ( ); const TrashIcon = () => ( ); const ArrowPathIcon = () => ( ); const NoSymbolIcon = () => ( ); const ExclamationTriangleIcon = () => ( ); // ============ Confirmation Modal ============ interface ConfirmationModalProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; title: string; message: string; confirmText: string; confirmButtonClass?: string; isLoading?: boolean; } function ConfirmationModal({ isOpen, onClose, onConfirm, title, message, confirmText, confirmButtonClass = 'bg-rose-500 hover:bg-rose-600', isLoading = false, }: ConfirmationModalProps) { const { t } = useTranslation(); if (!isOpen) return null; return (

{title}

{message}

); } // ============ User Actions Menu ============ type UserAction = 'delete' | 'resetTrial' | 'resetSubscription' | 'disable'; interface UserActionsMenuProps { user: UserListItem; onAction: (action: UserAction, user: UserListItem) => void; } function UserActionsMenu({ user, onAction }: UserActionsMenuProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const menuRef = useRef(null); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setIsOpen(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleAction = (action: UserAction) => { setIsOpen(false); onAction(action, user); }; const actions = [ { key: 'resetTrial' as const, label: t('admin.users.userActions.resetTrial'), icon: , className: 'text-blue-400 hover:bg-blue-500/10', }, { key: 'resetSubscription' as const, label: t('admin.users.userActions.resetSubscription'), icon: , className: 'text-amber-400 hover:bg-amber-500/10', }, { key: 'disable' as const, label: t('admin.users.userActions.disable'), icon: , className: 'text-dark-400 hover:bg-dark-700', }, { key: 'delete' as const, label: t('admin.users.userActions.delete'), icon: , className: 'text-rose-400 hover:bg-rose-500/10', }, ]; return (
{isOpen && (
{actions.map((action) => ( ))}
)}
); } // ============ 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-accent-500/20 text-accent-400 border-accent-500/30', green: 'bg-success-500/20 text-success-400 border-success-500/30', yellow: 'bg-warning-500/20 text-warning-400 border-warning-500/30', red: 'bg-error-500/20 text-error-400 border-error-500/30', purple: 'bg-accent-500/20 text-accent-400 border-accent-500/30', }; return (
{value}
{title}
{subtitle &&
{subtitle}
}
); } 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} ); } // ============ User List Component ============ interface UserRowProps { user: UserListItem; onClick: () => void; onAction: (action: UserAction, user: UserListItem) => void; formatAmount: (rubAmount: number) => string; } function UserRow({ user, onClick, onAction, formatAmount }: UserRowProps) { const { t } = useTranslation(); return (
{/* Avatar */}
{user.first_name?.[0] || user.username?.[0] || '?'}
{/* Info */}
{user.full_name} {user.username && @{user.username}}
{user.telegram_id} {user.status !== 'active' && } {user.has_subscription && user.subscription_status && ( {user.subscription_status === 'active' ? t('admin.users.status.subscription') : user.subscription_status === 'trial' ? t('admin.users.status.trial') : t('admin.users.status.expired')} )}
{/* Balance */}
{formatAmount(user.balance_rubles)}
{user.purchase_count > 0 ? t('admin.users.purchaseCount', { count: user.purchase_count }) : t('admin.users.noPurchases')}
{/* Actions Menu */}
); } // ============ Main Page ============ interface ConfirmModalState { isOpen: boolean; action: UserAction | null; user: UserListItem | null; } export default function AdminUsers() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); const { showToast } = useToast(); const [users, setUsers] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [emailSearch, setEmailSearch] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [sortBy, setSortBy] = useState('created_at'); const [offset, setOffset] = useState(0); const [total, setTotal] = useState(0); const [confirmModal, setConfirmModal] = useState({ isOpen: false, action: null, user: null, }); const [actionLoading, setActionLoading] = useState(false); const limit = 20; const loadUsers = useCallback(async () => { try { setLoading(true); const params: Record = { offset, limit, sort_by: sortBy }; if (search) params.search = search; if (emailSearch) params.email = emailSearch; if (statusFilter) params.status = statusFilter; const data = await adminUsersApi.getUsers( params as Parameters[0], ); setUsers(data.users); setTotal(data.total); } catch (error) { console.error('Failed to load users:', error); } finally { setLoading(false); } }, [offset, search, emailSearch, 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 handleUserAction = (action: UserAction, user: UserListItem) => { setConfirmModal({ isOpen: true, action, user }); }; const closeConfirmModal = () => { setConfirmModal({ isOpen: false, action: null, user: null }); }; const executeAction = async () => { const { action, user } = confirmModal; if (!action || !user) return; setActionLoading(true); try { let result: { success: boolean; message: string }; switch (action) { case 'delete': result = await adminUsersApi.deleteUser(user.id); break; case 'resetTrial': result = await adminUsersApi.resetTrial(user.id); break; case 'resetSubscription': result = await adminUsersApi.resetSubscription(user.id); break; case 'disable': result = await adminUsersApi.disableUser(user.id); break; default: throw new Error('Unknown action'); } if (result.success) { showToast({ type: 'success', title: t('common.success'), message: t(`admin.users.userActions.success.${action}`), }); loadUsers(); loadStats(); } else { showToast({ type: 'error', title: t('common.error'), message: result.message || t('admin.users.userActions.error'), }); } } catch (error) { console.error('Action failed:', error); showToast({ type: 'error', title: t('common.error'), message: t('admin.users.userActions.error'), }); } finally { setActionLoading(false); closeConfirmModal(); } }; const getConfirmModalContent = () => { const { action } = confirmModal; if (!action) return { title: '', message: '', confirmText: '', confirmButtonClass: '' }; const configs: Record< UserAction, { title: string; message: string; confirmText: string; confirmButtonClass: string } > = { delete: { title: t('admin.users.userActions.confirmDelete.title'), message: t('admin.users.userActions.confirmDelete.message'), confirmText: t('admin.users.userActions.delete'), confirmButtonClass: 'bg-rose-500 hover:bg-rose-600', }, resetTrial: { title: t('admin.users.userActions.confirmResetTrial.title'), message: t('admin.users.userActions.confirmResetTrial.message'), confirmText: t('admin.users.userActions.resetTrial'), confirmButtonClass: 'bg-blue-500 hover:bg-blue-600', }, resetSubscription: { title: t('admin.users.userActions.confirmResetSubscription.title'), message: t('admin.users.userActions.confirmResetSubscription.message'), confirmText: t('admin.users.userActions.resetSubscription'), confirmButtonClass: 'bg-amber-500 hover:bg-amber-600', }, disable: { title: t('admin.users.userActions.confirmDisable.title'), message: t('admin.users.userActions.confirmDisable.message'), confirmText: t('admin.users.userActions.disable'), confirmButtonClass: 'bg-dark-600 hover:bg-dark-500', }, }; return configs[action]; }; const totalPages = Math.ceil(total / limit); const currentPage = Math.floor(offset / limit) + 1; return (
{/* Header */}

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

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

{/* Stats */} {stats && (
)} {/* Filters */}
{/* Search fields row */}
setSearch(e.target.value)} placeholder={t('admin.users.search')} className="w-full rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-4 text-dark-100 placeholder-dark-500 focus:border-dark-600 focus:outline-none" />
setEmailSearch(e.target.value)} placeholder={t('admin.users.searchEmail')} className="w-full rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-4 text-dark-100 placeholder-dark-500 focus:border-dark-600 focus:outline-none" />
{/* Filters row */}
{/* Users list */}
{loading ? (
) : users.length === 0 ? (
{t('admin.users.noData')}
) : ( users.map((user) => ( navigate(`/admin/users/${user.id}`)} onAction={handleUserAction} formatAmount={(amount) => formatWithCurrency(amount)} /> )) )}
{/* Pagination */} {totalPages > 1 && (
{t('admin.users.pagination.showing', { from: offset + 1, to: Math.min(offset + limit, total), total, })}
{currentPage} / {totalPages}
)} {/* User action confirmation modal */}
); }